diff --git a/.env b/.env new file mode 100644 index 000000000..a90afdb33 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +DB_URL="mongodb://localhost:27017/TrustBridge" +JWT_SECRET="NetDpVFFq8H8tYtWT3bKjKzgTmsXPZ7mylWw/Z4t2PKC09bhJuL9aud0u4GPuGBojCDZ/XHrpHDBmPokRoTA==" +FRONTEND_URL=http://localhost:5173 +REDIS_URL=redis://localhost:6379 +VITE_BACKEND_URL=http://localhost:5000 diff --git a/index.html b/index.html index 55f905377..ec7d134aa 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,9 @@ - + - Business Nexus - Connect Entrepreneurs & Investors + TrustBridge AI diff --git a/package-lock.json b/package-lock.json index bf00121ba..6f4d81a69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,17 +8,23 @@ "name": "business-nexus", "version": "0.1.0", "dependencies": { - "axios": "^1.6.7", + "agora-rtc-sdk-ng": "^4.24.0", + "axios": "^1.13.0", "date-fns": "^3.3.1", + "framer-motion": "^12.23.24", + "jwt-decode": "^4.0.0", "lucide-react": "^0.344.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", - "react-hot-toast": "^2.4.1", - "react-router-dom": "^6.22.1" + "react-hot-toast": "^2.6.0", + "react-router-dom": "^6.22.1", + "recharts": "^3.5.0", + "socket.io-client": "^4.8.1" }, "devDependencies": { "@eslint/js": "^9.9.1", + "@types/node": "^24.3.1", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", @@ -34,6 +40,39 @@ "vite": "^5.4.2" } }, + "node_modules/@agora-js/media": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@agora-js/media/-/media-4.24.0.tgz", + "integrity": "sha512-foii2klr5+qonLznxN0ZZFejoxLt/W8do79wmIsADPZLw2uZjRP35m0lqUGiLXBKeQ8u3i4UygPzEdFaY26hrw==", + "license": "MIT", + "dependencies": { + "@agora-js/report": "4.24.0", + "@agora-js/shared": "4.24.0", + "agora-rte-extension": "^1.2.4", + "axios": "^1.8.3", + "webrtc-adapter": "8.2.0" + } + }, + "node_modules/@agora-js/report": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@agora-js/report/-/report-4.24.0.tgz", + "integrity": "sha512-MYbtkdY1Ls0KW0iagUzrPzyvqMWlyCWSC5odEb1SQaraAl7DJeDUkf91a3wxKzrjVah+LCxFxsS4lCFDxvKgNA==", + "license": "MIT", + "dependencies": { + "@agora-js/shared": "4.24.0", + "axios": "^1.8.3" + } + }, + "node_modules/@agora-js/shared": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@agora-js/shared/-/shared-4.24.0.tgz", + "integrity": "sha512-Vj67ZcTHZI+1ctWusrEPSSGLM3l6CFiAze/Bi8r7YHRMLivzhZR79nV6GiKvHS3muLAON2YAExznvjPIly6lcg==", + "license": "MIT", + "dependencies": { + "axios": "^1.8.3", + "ua-parser-js": "^0.7.34" + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -722,7 +761,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" }, @@ -737,7 +775,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -749,7 +786,6 @@ "version": "4.11.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", - "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -758,7 +794,6 @@ "version": "0.18.0", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", - "dev": true, "dependencies": { "@eslint/object-schema": "^2.1.4", "debug": "^4.3.1", @@ -772,7 +807,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", - "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -781,7 +815,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", - "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -804,7 +837,6 @@ "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, "engines": { "node": ">=18" }, @@ -816,7 +848,6 @@ "version": "9.12.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.12.0.tgz", "integrity": "sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==", - "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -825,7 +856,6 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", - "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -834,7 +864,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", - "dev": true, "dependencies": { "levn": "^0.4.1" }, @@ -846,7 +875,6 @@ "version": "0.19.0", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", - "dev": true, "engines": { "node": ">=18.18.0" } @@ -855,7 +883,6 @@ "version": "0.16.5", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", - "dev": true, "dependencies": { "@humanfs/core": "^0.19.0", "@humanwhocodes/retry": "^0.3.0" @@ -868,7 +895,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, "engines": { "node": ">=12.22" }, @@ -881,7 +907,6 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, "engines": { "node": ">=18.18" }, @@ -1000,6 +1025,32 @@ "node": ">=14" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.1.tgz", + "integrity": "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.2.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@remix-run/router": { "version": "1.23.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", @@ -1217,6 +1268,24 @@ "win32" ] }, + "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/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1258,29 +1327,100 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/@types/node": { + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", + "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } }, "node_modules/@types/prop-types": { "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "dev": true + "devOptional": true }, "node_modules/@types/react": { "version": "18.3.11", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz", "integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1295,6 +1435,12 @@ "@types/react": "*" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.8.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", @@ -1548,7 +1694,6 @@ "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -1560,16 +1705,37 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agora-rtc-sdk-ng": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/agora-rtc-sdk-ng/-/agora-rtc-sdk-ng-4.24.0.tgz", + "integrity": "sha512-2apG/07EtsuX21ncSF77q+dr6/kDgu9B/RpKtstCtaq46l4/Eraoecewi4zXRUCY3Im+8dzTIXx6jUwyPdxdHQ==", + "license": "MIT", + "dependencies": { + "@agora-js/media": "4.24.0", + "@agora-js/report": "4.24.0", + "@agora-js/shared": "4.24.0", + "agora-rte-extension": "^1.2.4", + "axios": "^1.8.3", + "formdata-polyfill": "^4.0.7", + "pako": "^2.1.0", + "ua-parser-js": "^0.7.34", + "webrtc-adapter": "8.2.0" + } + }, + "node_modules/agora-rte-extension": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/agora-rte-extension/-/agora-rte-extension-1.2.4.tgz", + "integrity": "sha512-0ovZz1lbe30QraG1cU+ji7EnQ8aUu+Hf3F+a8xPml3wPOyUQEK6CTdxV9kMecr9t+fIDrGeW7wgJTsM1DQE7Nw==", + "license": "ISC" + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1633,8 +1799,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/asynckit": { "version": "0.4.0", @@ -1689,21 +1854,20 @@ } }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.0.tgz", + "integrity": "sha512-zt40Pz4zcRXra9CVV31KeyofwiNvAbJ5B6YPz9pMJ+yOSLikvPT4Yi5LjfgjRa9CawVYBaD1JQzIVcIvBejKeA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/binary-extensions": { "version": "2.3.0", @@ -1721,7 +1885,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1788,7 +1951,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -1803,9 +1965,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001667", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz", - "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", + "version": "1.0.30001747", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001747.tgz", + "integrity": "sha512-mzFa2DGIhuc5490Nd/G31xN1pnBnYMadtkyTjefPI7wzypqgCEpeWu9bJr0OnDsyKrW75zA9ZAt7pbQFmwLsQg==", "dev": true, "funding": [ { @@ -1820,7 +1982,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "2.4.2", @@ -1872,6 +2035,15 @@ "node": ">= 6" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -1911,8 +2083,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/convert-source-map": { "version": "2.0.0", @@ -1924,7 +2095,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1951,6 +2121,127 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/date-fns": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", @@ -1965,7 +2256,6 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, "dependencies": { "ms": "^2.1.3" }, @@ -1978,11 +2268,16 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "node_modules/delayed-stream": { "version": "1.0.0", @@ -2037,6 +2332,28 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "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/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2082,6 +2399,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.42.0.tgz", + "integrity": "sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -2142,7 +2469,6 @@ "version": "9.12.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.12.0.tgz", "integrity": "sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==", - "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", @@ -2210,6 +2536,18 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, + "node_modules/eslint-plugin-react-perf": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-perf/-/eslint-plugin-react-perf-3.3.3.tgz", + "integrity": "sha512-EzPdxsRJg5IllCAH9ny/3nK7sv9251tvKmi/d3Ouv5KzI8TB3zNhzScxL9wnh9Hvv8GYC5LEtzTauynfOEYiAw==", + "license": "MIT", + "engines": { + "node": ">=6.9.1" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, "node_modules/eslint-plugin-react-refresh": { "version": "0.4.12", "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.12.tgz", @@ -2223,7 +2561,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", - "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -2239,7 +2576,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", - "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2251,7 +2587,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2266,7 +2601,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2282,7 +2616,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2293,14 +2626,12 @@ "node_modules/eslint/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -2312,7 +2643,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2321,7 +2651,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -2333,7 +2662,6 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", - "dev": true, "dependencies": { "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", @@ -2350,7 +2678,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -2362,7 +2689,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -2374,7 +2700,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "engines": { "node": ">=4.0" } @@ -2383,16 +2708,20 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -2425,14 +2754,12 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "node_modules/fastq": { "version": "1.17.1", @@ -2443,11 +2770,33 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, "dependencies": { "flat-cache": "^4.0.0" }, @@ -2483,7 +2832,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -2499,7 +2847,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -2511,8 +2858,7 @@ "node_modules/flatted": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" }, "node_modules/follow-redirects": { "version": "1.15.9", @@ -2551,20 +2897,33 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -2578,6 +2937,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", + "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2670,7 +3056,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -2792,16 +3177,24 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, "engines": { "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -2817,11 +3210,19 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "engines": { "node": ">=0.8.19" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2853,7 +3254,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -2871,7 +3271,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -2891,8 +3290,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/jackspeak": { "version": "3.4.3", @@ -2913,7 +3311,7 @@ "version": "1.21.6", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", - "dev": true, + "devOptional": true, "bin": { "jiti": "bin/jiti.js" } @@ -2927,7 +3325,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -2950,20 +3347,17 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, "node_modules/json5": { "version": "2.2.3", @@ -2977,11 +3371,19 @@ "node": ">=6" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, "dependencies": { "json-buffer": "3.0.1" } @@ -2990,7 +3392,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -3018,7 +3419,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -3032,8 +3432,7 @@ "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, "node_modules/loose-envify": { "version": "1.4.0", @@ -3119,7 +3518,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3136,11 +3534,25 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/mz": { "version": "2.7.0", @@ -3174,8 +3586,27 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } }, "node_modules/node-releases": { "version": "2.0.18", @@ -3222,7 +3653,6 @@ "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -3239,7 +3669,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -3254,7 +3683,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -3271,11 +3699,16 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -3287,7 +3720,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "engines": { "node": ">=8" } @@ -3296,7 +3728,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -3524,7 +3955,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, "engines": { "node": ">= 0.8.0" } @@ -3550,7 +3980,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -3616,9 +4045,9 @@ } }, "node_modules/react-hot-toast": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz", - "integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", "license": "MIT", "dependencies": { "csstype": "^3.1.3", @@ -3638,6 +4067,29 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -3700,6 +4152,58 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.5.0.tgz", + "integrity": "sha512-jWqBtu8L3VICXWa3g/y+bKjL8DDHSRme7DHD/70LQ/Tk0di1h11Y0kKC0nPh6YJ2oaa0k6anIFNhg6SfzHWdEA==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eslint-plugin-react-perf": "^3.3.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -3721,7 +4225,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -3802,6 +4305,12 @@ "loose-envify": "^1.1.0" } }, + "node_modules/sdp": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.1.tgz", + "integrity": "sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==", + "license": "MIT" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -3815,7 +4324,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -3827,7 +4335,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -3844,6 +4351,34 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "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.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3953,7 +4488,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "engines": { "node": ">=8" }, @@ -4047,8 +4581,7 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" }, "node_modules/thenify": { "version": "3.3.1", @@ -4071,6 +4604,12 @@ "node": ">=0.8" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -4120,7 +4659,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -4164,6 +4702,39 @@ } } }, + "node_modules/ua-parser-js": { + "version": "0.7.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz", + "integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", @@ -4198,22 +4769,53 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { - "version": "5.4.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", - "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "version": "5.4.20", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", + "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -4268,11 +4870,32 @@ } } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webrtc-adapter": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-8.2.0.tgz", + "integrity": "sha512-umxCMgedPAVq4Pe/jl3xmelLXLn4XZWFEMR5Iipb5wJ+k1xMX0yC4ZY9CueZUU1MjapFxai1tFGE7R/kotH6Ww==", + "license": "BSD-3-Clause", + "dependencies": { + "sdp": "^3.0.2" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -4287,7 +4910,6 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4413,6 +5035,35 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "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/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -4435,7 +5086,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "engines": { "node": ">=10" }, diff --git a/package.json b/package.json index cdfd37b29..f7f232203 100644 --- a/package.json +++ b/package.json @@ -10,17 +10,23 @@ "preview": "vite preview" }, "dependencies": { + "agora-rtc-sdk-ng": "^4.24.0", + "axios": "^1.13.0", + "date-fns": "^3.3.1", + "framer-motion": "^12.23.24", + "jwt-decode": "^4.0.0", "lucide-react": "^0.344.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.22.1", - "axios": "^1.6.7", - "date-fns": "^3.3.1", "react-dropzone": "^14.2.3", - "react-hot-toast": "^2.4.1" + "react-hot-toast": "^2.6.0", + "react-router-dom": "^6.22.1", + "recharts": "^3.5.0", + "socket.io-client": "^4.8.1" }, "devDependencies": { "@eslint/js": "^9.9.1", + "@types/node": "^24.3.1", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", @@ -34,5 +40,10 @@ "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", "vite": "^5.4.2" + }, + "husky": { + "hooks": { + "pre-commit": "npm run lint" + } } -} \ No newline at end of file +} diff --git a/public/TrustBridge-AI-Logo.png b/public/TrustBridge-AI-Logo.png new file mode 100644 index 000000000..58abfdab2 Binary files /dev/null and b/public/TrustBridge-AI-Logo.png differ diff --git a/public/app logo.jpeg b/public/app logo.jpeg new file mode 100644 index 000000000..9f3abdca7 Binary files /dev/null and b/public/app logo.jpeg differ diff --git a/src/App.tsx b/src/App.tsx index 51b12d8bf..3beffa515 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,104 +1,156 @@ -import React from 'react'; -import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; -import { AuthProvider } from './context/AuthContext'; +import { + BrowserRouter as Router, + Routes, + Route, + Navigate, +} from "react-router-dom"; +import { AuthProvider } from "./context/AuthContext"; +import { SocketProvider } from "./context/SocketContext"; // Layouts -import { DashboardLayout } from './components/layout/DashboardLayout'; +import { DashboardLayout } from "./components/layout/DashboardLayout"; // Auth Pages -import { LoginPage } from './pages/auth/LoginPage'; -import { RegisterPage } from './pages/auth/RegisterPage'; +import { LoginPage } from "./pages/auth/LoginPage"; +import { RegisterPage } from "./pages/auth/RegisterPage"; // Dashboard Pages -import { EntrepreneurDashboard } from './pages/dashboard/EntrepreneurDashboard'; -import { InvestorDashboard } from './pages/dashboard/InvestorDashboard'; +import { EntrepreneurDashboard } from "./pages/dashboard/EntrepreneurDashboard"; +import { InvestorDashboard } from "./pages/dashboard/InvestorDashboard"; +import { AdminDashboard } from "./pages/dashboard/AdminDashboard"; // Profile Pages -import { EntrepreneurProfile } from './pages/profile/EntrepreneurProfile'; -import { InvestorProfile } from './pages/profile/InvestorProfile'; +import { EntrepreneurProfile } from "./pages/profile/EntrepreneurProfile"; +import { InvestorProfile } from "./pages/profile/InvestorProfile"; // Feature Pages -import { InvestorsPage } from './pages/investors/InvestorsPage'; -import { EntrepreneursPage } from './pages/entrepreneurs/EntrepreneursPage'; -import { MessagesPage } from './pages/messages/MessagesPage'; -import { NotificationsPage } from './pages/notifications/NotificationsPage'; -import { DocumentsPage } from './pages/documents/DocumentsPage'; -import { SettingsPage } from './pages/settings/SettingsPage'; -import { HelpPage } from './pages/help/HelpPage'; -import { DealsPage } from './pages/deals/DealsPage'; +import { InvestorsPage } from "./pages/investors/InvestorsPage"; +import { EntrepreneursPage } from "./pages/entrepreneurs/EntrepreneursPage"; +import { MessagesPage } from "./pages/messages/MessagesPage"; +import { NotificationsPage } from "./pages/notifications/NotificationsPage"; +import { DocumentsPage } from "./pages/documents/DocumentsPage"; +import { SettingsPage } from "./pages/settings/SettingsPage"; +import { HelpPage } from "./pages/help/HelpPage"; +import { DealsPage } from "./pages/deals/DealsPage"; // Chat Pages -import { ChatPage } from './pages/chat/ChatPage'; +import { ChatPage } from "./pages/chat/ChatPage"; +import { ForgotPasswordPage } from "./pages/auth/ForgotPasswordPage"; +import { ResetPasswordPage } from "./pages/auth/ResetPasswordPage"; +//import { VideoCall } from "./components/webRTC/Videocall"; +//import { AudioCall } from "./components/webRTC/AudioCall"; +import { Toaster } from "react-hot-toast"; +import { + FraudAndRiskDetection, +} from "./pages/admin/FraudAndRiskDetection"; +import { Activities } from "./pages/admin/activities"; +import { Investors } from "./pages/admin/investors"; +import Campaigns from "./pages/admin/campaigns"; + +import { HomePage } from "./pages/home/HomePage"; +import { LoginWithOAuthPage } from "./pages/auth/LoginWithOAuthPage"; +import { UserDetails } from "./components/user/UserDetails"; +import { Supporters } from "./pages/admin/supporters"; +import { FlaggedAccounts } from "./pages/admin/flaggedAccounts"; +import { Users } from "./pages/admin/Users"; function App() { return ( - - - {/* Authentication Routes */} - } /> - } /> - - {/* Dashboard Routes */} - }> - } /> - } /> - - - {/* Profile Routes */} - }> - } /> - } /> - - - {/* Feature Routes */} - }> - } /> - - - }> - } /> - - - }> - } /> - - - }> - } /> - - - }> - } /> - - - }> - } /> - - - }> - } /> - - - }> - } /> - - - {/* Chat Routes */} - }> - } /> - } /> - - - {/* Redirect root to login */} - } /> - - {/* Catch all other routes and redirect to login */} - } /> - - + + + + {/* Authentication Routes */} + } /> + } /> + } /> + } /> + } /> + } /> + + {/* Dashboard Routes */} + }> + } /> + } /> + } /> + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } + /> + + + {/* Profile Routes */} + }> + } + /> + } /> + + + {/* Feature Routes */} + }> + } /> + + + }> + } /> + + + }> + } /> + + + }> + } /> + + + }> + } /> + + + }> + } /> + + + }> + } /> + + + }> + + }> + } /> + + + {/* Chat Routes */} + }> + } /> + + + {/* Redirect root to login */} + } /> + + {/* Catch all other routes and redirect to login */} + } + /> + + + + ); } -export default App; \ No newline at end of file +export default App; diff --git a/src/components/admin/FraudAndRiskDetectionChart.tsx b/src/components/admin/FraudAndRiskDetectionChart.tsx new file mode 100644 index 000000000..ab35ec7b3 --- /dev/null +++ b/src/components/admin/FraudAndRiskDetectionChart.tsx @@ -0,0 +1,55 @@ +import { + LineChart, + Line, + XAxis, + YAxis, + Tooltip, + CartesianGrid, + ResponsiveContainer, +} from "recharts"; + +const data = [ + { + riskScore: 10, + count: 6, + eventType: "failed_login", + }, + + { + riskScore: 13, + count: 7, + eventType: "multi time", + }, + { + riskScore: 13, + count: 20, + eventType: "failed_login", + }, + + { + riskScore: 20, + count: 4, + eventType: "multitime login", + }, +]; +export const FraudAndRiskDetectionChart = ({}) => { + return ( + + + + + + + + + + + + + ); +}; diff --git a/src/components/admin/FundingChart.tsx b/src/components/admin/FundingChart.tsx new file mode 100644 index 000000000..34812528f --- /dev/null +++ b/src/components/admin/FundingChart.tsx @@ -0,0 +1,35 @@ +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from "recharts"; + +const data = [ + { month: "Jan", funding: 20000 }, + { month: "Feb", funding: 80000 }, + { month: "Mar", funding: 40000 }, + { month: "Apr", funding: 100000 }, +]; + +export const FundingChart = () => { + return ( + + + + + + + + + + ); +}; diff --git a/src/components/admin/GraphViewModal.tsx b/src/components/admin/GraphViewModal.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/admin/StageDistributionChart.tsx b/src/components/admin/StageDistributionChart.tsx new file mode 100644 index 000000000..f3e601307 --- /dev/null +++ b/src/components/admin/StageDistributionChart.tsx @@ -0,0 +1,31 @@ +import { PieChart, Pie, Tooltip, Cell, ResponsiveContainer } from "recharts"; + +const data = [ + { name: "Idea", value: 10 }, + { name: "MVP", value: 25 }, + { name: "Seed", value: 15 }, + { name: "Series A", value: 8 }, +]; + +const COLORS = ["#6366f1", "#22c55e", "#f97316", "#3b82f6"]; + +export const StageDistributionChart = () => { + return ( + + + + {data.map((_, i) => ( + + ))} + + + + + ); +}; diff --git a/src/components/admin/StartupGrowthChart.tsx b/src/components/admin/StartupGrowthChart.tsx new file mode 100644 index 000000000..c0ef4de41 --- /dev/null +++ b/src/components/admin/StartupGrowthChart.tsx @@ -0,0 +1,49 @@ +import { + LineChart, + Line, + XAxis, + YAxis, + Tooltip, + CartesianGrid, + ResponsiveContainer, +} from "recharts"; + +type ChartData = { + month: string; + inv?: number; + ent?: number; +}; + +type Props = { + data: ChartData[]; +}; + +export const StartupGrowthChart: React.FC = ({ data }) => { + return ( +
+ + + + + + + + + + + +
+ ); +}; diff --git a/src/components/admin/StartupIndustryChart.tsx b/src/components/admin/StartupIndustryChart.tsx new file mode 100644 index 000000000..fce5b88a2 --- /dev/null +++ b/src/components/admin/StartupIndustryChart.tsx @@ -0,0 +1,23 @@ +import { + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + CartesianGrid, + ResponsiveContainer, +} from "recharts"; + +export const StartupIndustryChart = ({ data }) => { + return ( + + + + + + + + + + ); +}; diff --git a/src/components/camp/CampForm.tsx b/src/components/camp/CampForm.tsx new file mode 100644 index 000000000..0108a87c6 --- /dev/null +++ b/src/components/camp/CampForm.tsx @@ -0,0 +1,313 @@ +import React, { useState } from "react"; +import axios from "axios"; +import toast from "react-hot-toast"; +import { motion } from "framer-motion"; + +const URL = import.meta.env.VITE_BACKEND_URL; + + +interface CampFormProps { + onSuccess: () => void; +} + +const CampForm: React.FC = ({ onSuccess }) => { + const [formData, setFormData] = useState({ + title: "", + description: "", + goalAmount: "", + startDate: "", + endDate: "", + category: "Other", + }); + + const [images, setImages] = useState(null); + const [previewUrls, setPreviewUrls] = useState([]); + const [loading, setLoading] = useState(false); + + const handleFileChange = (e: React.ChangeEvent) => { + if (e.target.files) { + setImages(e.target.files); + setPreviewUrls(Array.from(e.target.files).map((file) => URL.createObjectURL(file))); + } + }; + + const handleChange = ( + e: React.ChangeEvent< + HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement + > + ) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + const validateForm = () => { + const { title, description, goalAmount, startDate, endDate, category } = formData; + + // Title validations + if (!title.trim()) { + toast.error("Title is required"); + return false; + } + + // Only letters and spaces allowed for title + if (!/^[A-Za-z\s]+$/.test(title)) { + toast.error("Title must contain only letters and spaces"); + return false; + } + + // Description validations + if (!description.trim()) { + toast.error("Description is required"); + return false; + } + + // Only letters and spaces allowed for description + if (!/^[A-Za-z\s]+$/.test(description)) { + toast.error("Description must contain only letters and spaces"); + return false; + } + + // Minimum 30 words validation + const wordCount = description.trim().split(/\s+/).length; + if (wordCount < 30) { + toast.error("Description must be at least 30 words"); + return false; + } + + // Goal amount validation + if (!goalAmount || isNaN(Number(goalAmount)) || Number(goalAmount) <= 0) { + toast.error("Goal amount must be a positive number"); + return false; + } + + // Date validations + if (!startDate) { + toast.error("Start date is required"); + return false; + } + + if (!endDate) { + toast.error("End date is required"); + return false; + } + + if (new Date(startDate) > new Date(endDate)) { + toast.error("Start date cannot be after end date"); + return false; + } + + // Category validation + if (!category) { + toast.error("Category is required"); + return false; + } + + // Images validation + if (!images || images.length === 0) { + toast.error("At least one image is required"); + return false; + } + + // Optional: Validate image types and sizes + for (let i = 0; i < images.length; i++) { + const file = images[i]; + if (!file.type.startsWith("image/")) { + toast.error(`File ${file.name} is not an image`); + return false; + } + if (file.size > 5 * 1024 * 1024) { + toast.error(`File ${file.name} exceeds 5MB`); + return false; + } + } + + return true; +}; + + + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) return; + + setLoading(true); + + try { + const data = new FormData(); + Object.entries(formData).forEach(([key, value]) => + data.append(key, value) + ); + + if (images) { + for (let i = 0; i < images.length; i++) { + data.append("images", images[i]); + } + } + + await axios.post(`${URL}/admin/campaigns`, data, { + headers: { "Content-Type": "multipart/form-data" }, + }); + + toast.success("🎉 Campaign created successfully!"); + setFormData({ + title: "", + description: "", + goalAmount: "", + startDate: "", + endDate: "", + category: "Other", + }); + setImages(null); + setPreviewUrls([]); + onSuccess(); + } catch (error: any) { + console.error(error); + toast.error( + error.response?.data?.message || + error.message || + "Failed to create campaign" + ); + } finally { + setLoading(false); + } + }; + + return ( + + + 🚀 Add New Campaign + + + {/* Title */} + + + {/* Description */} + + + {/* Goal Amount */} + + + {/* Dates */} +
+ + +
+ + {/* Category */} + + + + + + + + + {/* File Upload */} + + + + + {/* Preview Images */} + {previewUrls.length > 0 && ( +
+ {previewUrls.map((url, idx) => ( + + ))} +
+ )} + + {/* Submit Button */} + + {loading ? "⏳ Creating..." : "Create Campaign"} + +
+ ); +}; + +export default CampForm; diff --git a/src/components/chat/ChatMessage.tsx b/src/components/chat/ChatMessage.tsx index c3cf93ef4..f94922007 100644 --- a/src/components/chat/ChatMessage.tsx +++ b/src/components/chat/ChatMessage.tsx @@ -1,22 +1,26 @@ -import React from 'react'; -import { formatDistanceToNow } from 'date-fns'; -import { Message } from '../../types'; -import { Avatar } from '../ui/Avatar'; -import { findUserById } from '../../data/users'; +import React from "react"; +import { formatDistanceToNow } from "date-fns"; +import { Message, User } from "../../types"; +import { Avatar } from "../ui/Avatar"; interface ChatMessageProps { message: Message; + user: User | undefined; isCurrentUser: boolean; } -export const ChatMessage: React.FC = ({ message, isCurrentUser }) => { - const user = findUserById(message.senderId); - +export const ChatMessage: React.FC = ({ + message, + user, + isCurrentUser, +}) => { if (!user) return null; - + return (
{!isCurrentUser && ( = ({ message, isCurrentUser className="mr-2 self-end" /> )} - -
+ +

{message.content}

- - - {formatDistanceToNow(new Date(message.timestamp), { addSuffix: true })} - + {message?.time && ( + + {formatDistanceToNow(new Date(message.time), { addSuffix: true })} + + )}
- + {isCurrentUser && ( = ({ message, isCurrentUser )}
); -}; \ No newline at end of file +}; diff --git a/src/components/chat/ChatUserList.tsx b/src/components/chat/ChatUserList.tsx index 62a295101..2745b7e3e 100644 --- a/src/components/chat/ChatUserList.tsx +++ b/src/components/chat/ChatUserList.tsx @@ -1,87 +1,128 @@ -import React from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { formatDistanceToNow } from 'date-fns'; -import { ChatConversation } from '../../types'; -import { Avatar } from '../ui/Avatar'; -import { Badge } from '../ui/Badge'; -import { findUserById } from '../../data/users'; -import { useAuth } from '../../context/AuthContext'; +import React, { useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { formatDistanceToNow } from "date-fns"; +import { ChatConversation, User } from "../../types"; +import { Avatar } from "../ui/Avatar"; +import { Badge } from "../ui/Badge"; +import { useAuth } from "../../context/AuthContext"; +import { getUserFromDb } from "../../data/users"; interface ChatUserListProps { - conversations: ChatConversation[]; + conversation: ChatConversation; } -export const ChatUserList: React.FC = ({ conversations }) => { +export const ChatUserList: React.FC = ({ conversation }) => { const navigate = useNavigate(); const { userId: activeUserId } = useParams<{ userId: string }>(); const { user: currentUser } = useAuth(); - + + const [chatPartners, setChatPartners] = useState([]); + + useEffect(() => { + const fetchParticipants = async () => { + if (!conversation?.participants) return; + + const allIds = Array.from( + new Set(conversation.participants.flatMap((i) => i.receiverId || "")) + ); + const users = await Promise.all( + allIds.map(async (id) => await getUserFromDb(id)) + ); + + // filter nulls (if any user not found) + setChatPartners(users.filter(Boolean) as User[]); + + // find the index of active user last message + }; + + fetchParticipants(); + }, [conversation, currentUser, activeUserId]); + + if (conversation === null) return; + if (!currentUser) return null; - + const handleSelectUser = (userId: string) => { navigate(`/chat/${userId}`); }; return ( -
+
-

Messages

- +

+ Messages +

+
- {conversations.length > 0 ? ( - conversations.map(conversation => { - // Get the other participant (not the current user) - const otherParticipantId = conversation.participants.find(id => id !== currentUser.id); - if (!otherParticipantId) return null; - - const otherUser = findUserById(otherParticipantId); - if (!otherUser) return null; - - const lastMessage = conversation.lastMessage; - const isActive = activeUserId === otherParticipantId; - + {chatPartners.length > 0 ? ( + chatPartners.map((user) => { + const isActive = user._id === activeUserId; // highlight current open chat + let lastMessage; + const lastMessageIndex = conversation.participants.findIndex( + (part) => { + return part.receiverId === user._id; + } + ); + if (lastMessageIndex === -1) { + return; + } else { + lastMessage = { + ...conversation.participants[lastMessageIndex].lastMessage, + }; + } + return (
handleSelectUser(otherUser.id)} + onClick={() => handleSelectUser(user._id)} > - +

- {otherUser.name} + {user.name.slice(0, 5)} + {"..."}

- - {lastMessage && ( - - {formatDistanceToNow(new Date(lastMessage.timestamp), { addSuffix: false })} - - )} + + {Object.keys(lastMessage).length !== 0 && + lastMessage?.time && ( + + {formatDistanceToNow(new Date(lastMessage.time), { + addSuffix: false, + })} + + )}
- +
{lastMessage && (

- {lastMessage.senderId === currentUser.id ? 'You: ' : ''} + {lastMessage.senderId === currentUser.userId + ? "You: " + : ""} {lastMessage.content}

)} - - {lastMessage && !lastMessage.isRead && lastMessage.senderId !== currentUser.id && ( - New - )} + + {lastMessage && + !lastMessage.isRead && + lastMessage.senderId !== currentUser.userId && ( + + New + + )}
@@ -96,4 +137,4 @@ export const ChatUserList: React.FC = ({ conversations }) =>
); -}; \ No newline at end of file +}; diff --git a/src/components/collaboration/CollaborationRequestCard.tsx b/src/components/collaboration/CollaborationRequestCard.tsx index 192897711..32c0b6176 100644 --- a/src/components/collaboration/CollaborationRequestCard.tsx +++ b/src/components/collaboration/CollaborationRequestCard.tsx @@ -1,14 +1,14 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Check, X, MessageCircle } from 'lucide-react'; -import { CollaborationRequest } from '../../types'; +import { CollaborationRequest, Investor } from '../../types'; import { Card, CardBody, CardFooter } from '../ui/Card'; import { Avatar } from '../ui/Avatar'; import { Badge } from '../ui/Badge'; import { Button } from '../ui/Button'; -import { findUserById } from '../../data/users'; import { updateRequestStatus } from '../../data/collaborationRequests'; import { formatDistanceToNow } from 'date-fns'; +import { getInvestorById } from '../../data/users'; interface CollaborationRequestCardProps { request: CollaborationRequest; @@ -20,34 +20,42 @@ export const CollaborationRequestCard: React.FC = onStatusUpdate }) => { const navigate = useNavigate(); - const investor = findUserById(request.investorId); + const [investor, setInvestor] = useState(); + useEffect(() => { + const fetchInvestors = async () => { + const investor = await getInvestorById(request.inves_id); + setInvestor(investor); + }; + fetchInvestors(); + }, []); + if (!investor) return null; - const handleAccept = () => { - updateRequestStatus(request.id, 'accepted'); +const handleAccept = () => { + updateRequestStatus(request._id, 'accepted'); if (onStatusUpdate) { - onStatusUpdate(request.id, 'accepted'); + onStatusUpdate(request._id, 'accepted'); } }; const handleReject = () => { - updateRequestStatus(request.id, 'rejected'); + updateRequestStatus(request._id, 'rejected'); if (onStatusUpdate) { - onStatusUpdate(request.id, 'rejected'); + onStatusUpdate(request._id, 'rejected'); } }; const handleMessage = () => { - navigate(`/chat/${investor.id}`); + navigate(`/chat/${investor.userId}`); }; const handleViewProfile = () => { - navigate(`/profile/investor/${investor.id}`); + navigate(`/profile/investor/${investor.userId}`); }; const getStatusBadge = () => { - switch (request.status) { + switch (request.requestStatus) { case 'pending': return Pending; case 'accepted': @@ -75,7 +83,7 @@ export const CollaborationRequestCard: React.FC =

{investor.name}

- {formatDistanceToNow(new Date(request.createdAt), { addSuffix: true })} + {formatDistanceToNow(new Date(request.time), { addSuffix: true })}

@@ -89,7 +97,7 @@ export const CollaborationRequestCard: React.FC = - {request.status === 'pending' ? ( + {request.requestStatus === 'pending' ? (
- +
- +
-

Investment Interests

+

+ Investment Interests +

- {investor.investmentInterests.map((interest, index) => ( - {interest} - ))} + {investor.investmentInterests && + investor.investmentInterests.map((interest, index) => ( + + {interest} + + ))}
- +

{investor.bio}

- +
Investment Range -

{investor.minimumInvestment} - {investor.maximumInvestment}

+

+ {investor.minimumInvestment} - {investor.maximumInvestment} +

- + {showActions && ( - + - - + - {user.name} + + {user.name} +
) : ( @@ -113,7 +165,7 @@ export const Navbar: React.FC = () => {
)}
- + {/* Mobile menu button */}
- + {/* Mobile menu */} {isMenuOpen && (
@@ -141,14 +193,18 @@ export const Navbar: React.FC = () => { src={user.avatarUrl} alt={user.name} size="sm" - status={user.isOnline ? 'online' : 'offline'} + status={user.isOnline ? "online" : "offline"} />
-

{user.name}

-

{user.role}

+

+ {user.name} +

+

+ {user.role} +

- +
{navLinks.map((link, index) => ( { {link.text} ))} - + + - setIsMenuOpen(false)} > @@ -197,4 +255,4 @@ export const Navbar: React.FC = () => { )} ); -}; \ No newline at end of file +}; diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 842c11da0..6a55a58b1 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,10 +1,23 @@ -import React from 'react'; -import { NavLink } from 'react-router-dom'; -import { useAuth } from '../../context/AuthContext'; -import { - Home, Building2, CircleDollarSign, Users, MessageCircle, - Bell, FileText, Settings, HelpCircle -} from 'lucide-react'; +import React from "react"; +import { NavLink } from "react-router-dom"; +import { useAuth } from "../../context/AuthContext"; +import { + Home, + Building2, + CircleDollarSign, + Users, + MessageCircle, + Bell, + FileText, + Settings, + HelpCircle, + AlertTriangle, + Rocket, + Shield, + Users2Icon, + Briefcase, + ShieldBan, +} from "lucide-react"; interface SidebarItemProps { to: string; @@ -16,11 +29,11 @@ const SidebarItem: React.FC = ({ to, icon, text }) => { return ( + className={({ isActive }) => `flex items-center py-2.5 px-4 rounded-md transition-colors duration-200 ${ - isActive - ? 'bg-primary-50 text-primary-700' - : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900' + isActive + ? "bg-primary-50 text-primary-700" + : "text-gray-600 hover:bg-gray-100 hover:text-gray-900" }` } > @@ -32,36 +45,101 @@ const SidebarItem: React.FC = ({ to, icon, text }) => { export const Sidebar: React.FC = () => { const { user } = useAuth(); - + if (!user) return null; - - // Define sidebar items based on user role + + // Entrepreneur Sidebar const entrepreneurItems = [ - { to: '/dashboard/entrepreneur', icon: , text: 'Dashboard' }, - { to: '/profile/entrepreneur/' + user.id, icon: , text: 'My Startup' }, - { to: '/investors', icon: , text: 'Find Investors' }, - { to: '/messages', icon: , text: 'Messages' }, - { to: '/notifications', icon: , text: 'Notifications' }, - { to: '/documents', icon: , text: 'Documents' }, + { + to: "/dashboard/entrepreneur", + icon: , + text: "Dashboard", + }, + { + to: "/profile/entrepreneur/" + user.userId, + icon: , + text: "My Startup", + }, + { + to: "/investors", + icon: , + text: "Find Investors", + }, + { to: "/messages", icon: , text: "Messages" }, + { to: "/notifications", icon: , text: "Notifications" }, + { to: "/documents", icon: , text: "Documents" }, ]; - + + // Investor Sidebar const investorItems = [ - { to: '/dashboard/investor', icon: , text: 'Dashboard' }, - { to: '/profile/investor/' + user.id, icon: , text: 'My Portfolio' }, - { to: '/entrepreneurs', icon: , text: 'Find Startups' }, - { to: '/messages', icon: , text: 'Messages' }, - { to: '/notifications', icon: , text: 'Notifications' }, - { to: '/deals', icon: , text: 'Deals' }, + { to: "/dashboard/investor", icon: , text: "Dashboard" }, + { + to: "/profile/investor/" + user.userId, + icon: , + text: "My Portfolio", + }, + { to: "/entrepreneurs", icon: , text: "Find Startups" }, + { to: "/messages", icon: , text: "Messages" }, + { to: "/notifications", icon: , text: "Notifications" }, + { to: "/deals", icon: , text: "Deals" }, + ]; + + // Admin Sidebar + const adminItems = [ + { + to: "/dashboard/admin", + icon: , + text: "Dashboard Overview", + }, + + { + to: "/admin/all-users", + icon: , + text: "Manage Users", + }, + { + to: "/admin/investors", + icon: , + text: "Manage Investors", + }, + { + to: "/admin/supporters", + icon: , + text: "Manage Supporters", + }, + + { + to: "/admin/campaigns", + icon: , + text: "Active Campaigns", + }, + { + to: "/admin/fraud-and-risk-detection", + icon: , + text: "Fraud and Risk Detection", + }, + { + to: "/admin/flaggedAccounts", + icon: , + text: "Flagged Accounts", + }, ]; - - const sidebarItems = user.role === 'entrepreneur' ? entrepreneurItems : investorItems; - - // Common items at the bottom + const commonItems = [ - { to: '/settings', icon: , text: 'Settings' }, - { to: '/help', icon: , text: 'Help & Support' }, + { to: "/settings", icon: , text: "Settings" }, + { to: "/help", icon: , text: "Help & Support" }, ]; - + + // Role-based sidebar items + const sidebarItems = + user.role === "entrepreneur" + ? entrepreneurItems + : user.role === "investor" + ? investorItems + : user.role === "admin" + ? adminItems + : []; + return (
@@ -76,7 +154,7 @@ export const Sidebar: React.FC = () => { /> ))}
- +

Settings @@ -93,13 +171,15 @@ export const Sidebar: React.FC = () => {

- + ); -}; \ No newline at end of file +}; diff --git a/src/components/settings/AppearanceSettings.tsx b/src/components/settings/AppearanceSettings.tsx new file mode 100644 index 000000000..a319e315b --- /dev/null +++ b/src/components/settings/AppearanceSettings.tsx @@ -0,0 +1,271 @@ +import React, { useState, useEffect } from "react"; +import { Card, CardHeader, CardBody } from "../ui/Card"; +import { Button } from "../ui/Button"; +import { Sun, Moon, Monitor, Check } from "lucide-react"; + +type Theme = "light" | "dark" | "system"; +type FontSize = "small" | "medium" | "large"; + +export const AppearanceSettings: React.FC = () => { + const [selectedTheme, setSelectedTheme] = useState(() => { + const saved = localStorage.getItem("theme"); + return (saved as Theme) || "light"; + }); + + const [fontSize, setFontSize] = useState(() => { + const saved = localStorage.getItem("fontSize"); + return (saved as FontSize) || "medium"; + }); + + const [highContrast, setHighContrast] = useState(() => { + const saved = localStorage.getItem("highContrast"); + return saved === "true"; + }); + + const [reduceMotion, setReduceMotion] = useState(() => { + const saved = localStorage.getItem("reduceMotion"); + return saved === "true"; + }); + + const themes = [ + { + id: "light" as Theme, + label: "Light", + icon: Sun, + description: "Clean and bright interface", + preview: "bg-white border-gray-300", + }, + { + id: "dark" as Theme, + label: "Dark", + icon: Moon, + description: "Easy on the eyes in low light", + preview: "bg-gray-900 border-gray-700", + }, + { + id: "system" as Theme, + label: "System", + icon: Monitor, + description: "Follows your system settings", + preview: "bg-gradient-to-br from-white to-gray-900 border-gray-500", + }, + ]; + + const fontSizes = [ + { + id: "small" as FontSize, + label: "Small", + size: "14px", + description: "Compact and space-efficient" + }, + { + id: "medium" as FontSize, + label: "Medium", + size: "16px", + description: "Recommended for most users" + }, + { + id: "large" as FontSize, + label: "Large", + size: "18px", + description: "Better readability" + }, + ]; + + // Apply theme changes + useEffect(() => { + localStorage.setItem("theme", selectedTheme); + + const root = document.documentElement; + + if (selectedTheme === "system") { + const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + root.classList.toggle("dark", isDark); + } else { + root.classList.toggle("dark", selectedTheme === "dark"); + } + + // TODO: Implement actual theme switching logic + console.log("Theme changed to:", selectedTheme); + }, [selectedTheme]); + + // Apply font size changes + useEffect(() => { + localStorage.setItem("fontSize", fontSize); + + const root = document.documentElement; + const sizeMap = { + small: "14px", + medium: "16px", + large: "18px", + }; + + root.style.fontSize = sizeMap[fontSize]; + console.log("Font size changed to:", fontSize); + }, [fontSize]); + + // Apply high contrast + useEffect(() => { + localStorage.setItem("highContrast", highContrast.toString()); + document.documentElement.classList.toggle("high-contrast", highContrast); + console.log("High contrast:", highContrast); + }, [highContrast]); + + // Apply reduce motion + useEffect(() => { + localStorage.setItem("reduceMotion", reduceMotion.toString()); + document.documentElement.classList.toggle("reduce-motion", reduceMotion); + console.log("Reduce motion:", reduceMotion); + }, [reduceMotion]); + + const handleThemeChange = (theme: Theme) => { + setSelectedTheme(theme); + }; + + const handleFontSizeChange = (size: FontSize) => { + setFontSize(size); + }; + + return ( +
+ {/* Theme Selection */} + + +

Theme

+

+ Choose how you want the interface to look +

+
+ +
+ {themes.map((theme) => { + const Icon = theme.icon; + const isSelected = selectedTheme === theme.id; + return ( + + ); + })} +
+
+
+ + {/* Font Size */} + + +

Font Size

+

+ Adjust the text size for better readability +

+
+ +
+ {fontSizes.map((size) => { + const isSelected = fontSize === size.id; + return ( + + ); + })} +
+ +
+

+ Preview: This text is displayed in your current selected font size setting. +

+
+
+
+ + + {/* Reset Settings */} + + +

+ Reset Appearance +

+
+ +
+

+ Reset all appearance settings to their default values +

+ +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/settings/BillingSettings.tsx b/src/components/settings/BillingSettings.tsx new file mode 100644 index 000000000..67f5e8562 --- /dev/null +++ b/src/components/settings/BillingSettings.tsx @@ -0,0 +1,258 @@ +import React, { useState } from "react"; +import { Card, CardHeader, CardBody } from "../ui/Card"; +import { Button } from "../ui/Button"; +import { Badge } from "../ui/Badge"; +import { CreditCard, Download } from "lucide-react"; + +interface PaymentMethod { + id: string; + type: string; + last4: string; + expiryMonth: number; + expiryYear: number; + isDefault: boolean; +} + +interface Invoice { + id: string; + date: string; + amount: number; + status: "paid" | "pending" | "failed"; + description: string; +} + +export const BillingSettings: React.FC = () => { + const [paymentMethods, setPaymentMethods] = useState([ + { + id: "1", + type: "Visa", + last4: "4242", + expiryMonth: 12, + expiryYear: 2025, + isDefault: true, + }, + { + id: "2", + type: "Mastercard", + last4: "8888", + expiryMonth: 6, + expiryYear: 2026, + isDefault: false, + }, + ]); + + const [invoices] = useState([ + { + id: "INV-001", + date: "2025-11-01", + amount: 29.99, + status: "paid", + description: "Monthly Subscription - November 2025", + }, + { + id: "INV-002", + date: "2025-10-01", + amount: 29.99, + status: "paid", + description: "Monthly Subscription - October 2025", + }, + { + id: "INV-003", + date: "2025-09-01", + amount: 29.99, + status: "paid", + description: "Monthly Subscription - September 2025", + }, + ]); + + const handleSetDefault = (id: string) => { + setPaymentMethods((prev) => + prev.map((method) => ({ + ...method, + isDefault: method.id === id, + })) + ); + }; + + const handleRemoveCard = (id: string) => { + setPaymentMethods((prev) => prev.filter((method) => method.id !== id)); + }; + + const handleDownloadInvoice = (invoiceId: string) => { + console.log("Downloading invoice:", invoiceId); + }; + + return ( +
+ + + +
+

+ Payment Methods +

+ +
+
+ +
+ {paymentMethods.map((method) => ( +
+
+
+ +
+
+
+

+ {method.type} •••• {method.last4} +

+ {method.isDefault && ( + Default + )} +
+

+ Expires {method.expiryMonth}/{method.expiryYear} +

+
+
+
+ {!method.isDefault && ( + + )} + +
+
+ ))} +
+
+
+ + + +

+ Billing History +

+
+ +
+ {invoices.map((invoice) => ( +
+
+
+

+ {invoice.id} +

+ + {invoice.status} + +
+

{invoice.description}

+

{invoice.date}

+
+
+ + ${invoice.amount.toFixed(2)} + + +
+
+ ))} +
+
+
+ + + +

+ Billing Information +

+
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/settings/EntrepreneurSettings.tsx b/src/components/settings/EntrepreneurSettings.tsx new file mode 100644 index 000000000..bdb937387 --- /dev/null +++ b/src/components/settings/EntrepreneurSettings.tsx @@ -0,0 +1,230 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { Input } from "../../components/ui/Input"; +import { Button } from "../../components/ui/Button"; +import { + getEnterpreneurById, + sendMailToUser, + updateEntrepreneurData, +} from "../../data/users"; +import { Entrepreneur, UserRole } from "../../types"; +import { useAuth } from "../../context/AuthContext"; +import { useLocation, useNavigate } from "react-router-dom"; +import { Card, CardHeader } from "../ui/Card"; + +type User = { + name: string; + email: string; + password: string; + role: UserRole; +}; + +export const EntrepreneurSettings: React.FC = () => { + const { user, register } = useAuth(); + const location = useLocation(); + const navigate = useNavigate(); + const checkIfSettingPage = location.pathname === "/settings" ? true : false; + const [entrepreneur, setEnterpreneur] = useState(); + + useEffect(() => { + const fetchEntrepreneur = async () => { + if (user?.userId) { + const entrepreneur = await getEnterpreneurById(user?.userId); + setEnterpreneur(entrepreneur); + } + }; + fetchEntrepreneur(); + }, [user]); + + const initialData = useMemo( + () => ({ + userId: entrepreneur?.userId, + startupName: entrepreneur?.startupName, + pitchSummary: entrepreneur?.pitchSummary, + fundingNeeded: entrepreneur?.fundingNeeded, + industry: entrepreneur?.industry, + foundedYear: entrepreneur?.foundedYear, + teamSize: entrepreneur?.teamSize, + revenue: entrepreneur?.revenue, + profitMargin: entrepreneur?.profitMargin, + growthRate: entrepreneur?.growthRate, + marketOpportunity: entrepreneur?.marketOpportunity, + advantage: entrepreneur?.advantage, + }), + [entrepreneur] + ); + + // formData can be a partial Entrepreneur while editing + const [formData, setFormData] = useState>( + initialData || {} + ); + + useEffect(() => { + setFormData(initialData || {}); + }, [initialData]); + + const handleUserChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData( + (prev) => ({ ...(prev || {}), [name]: value } as Partial) + ); + }; + + const handleUserSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + if (!checkIfSettingPage) { + const userInfoString = localStorage.getItem("userInfo"); + if (!userInfoString) { + return; + } + try { + const parsed = JSON.parse(userInfoString) as User; + const { name, email, password, role } = parsed; + const userId = await register(name, email, password, role); + console.log(userId); + if (userId) { + await updateEntrepreneurData({ ...formData, userId: userId }); + + const message = ` +

Hello,

+

Your account is currently under review by our administrators. You will be notified about your account activation within 24 hours.

+

If you have any questions, reply to this email or contact support at trustbridgeai@gmail.

+

Thank you for your patience.

+

Regards
TrustBridgeAi Support Team

+ `; + const sub = "Under Review Account Activation "; + sendMailToUser(message, sub, email); + + navigate("/", { replace: true }); + } + } catch (e) { + console.error("Failed to parse userInfo from localStorage", e); + } finally { + localStorage.removeItem("userInfo"); + } + } else { + await updateEntrepreneurData(formData); + + const fd = formData || {}; + const { + startupName, + pitchSummary, + fundingNeeded, + industry, + foundedYear, + teamSize, + marketOpportunity, + advantage, + revenue, + profitMargin, + growthRate, + } = fd as Partial; + + // Ensure userId is a string when updating entrepreneur state + setEnterpreneur({ + startupName, + pitchSummary, + fundingNeeded, + industry, + foundedYear, + teamSize, + marketOpportunity, + advantage, + revenue, + profitMargin, + growthRate, + } as Entrepreneur); + + setFormData(initialData || {}); + } + } catch (error) { + console.error(error); + } + }; + + return ( +
+ + Fill the Details as an Entrepreneur... + +
+
+
+ + + + + + +
+
+ + + + + + +
+
+
+ +
+
+
+ ); +}; diff --git a/src/components/settings/InvestorSettings.tsx b/src/components/settings/InvestorSettings.tsx new file mode 100644 index 000000000..680654af5 --- /dev/null +++ b/src/components/settings/InvestorSettings.tsx @@ -0,0 +1,309 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { useAuth } from "../../context/AuthContext"; +import { Investor, UserRole } from "../../types"; +import { + getInvestorById, + sendMailToUser, + updateInvestorData, +} from "../../data/users"; +import { Button } from "../ui/Button"; +import { Input } from "../ui/Input"; +import { Eraser } from "lucide-react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { Card, CardHeader } from "../ui/Card"; + +type User = { + name: string; + email: string; + password: string; + role: UserRole; +}; + +export const InvestorSettings: React.FC = () => { + const location = useLocation(); + const navigate = useNavigate(); + const checkIfSettingPage = location.pathname === "/settings" ? true : false; + const { user, register } = useAuth(); + + const [investor, setInvestor] = useState(); + const initialInvestorData = useMemo( + () => ({ + userId: investor?.userId || user?.userId, + investmentInterests: investor?.investmentInterests || [], + minimumInvestment: investor?.minimumInvestment, + totalInvestments: investor?.totalInvestments, + maximumInvestment: investor?.maximumInvestment, + investmentCriteria: investor?.investmentCriteria || [], + successfullExits: investor?.successfullExits, + minTimline: investor?.minTimline, + maxTimline: investor?.maxTimline, + interest: "", + criteria: "", + }), + [investor, user] + ); + + const [investorFormData, setInvestorFormData] = useState(initialInvestorData); + // Fetch investor data + useEffect(() => { + const fetchInvestors = async () => { + if (user?.userId) { + const investor = await getInvestorById(user?.userId); + setInvestor(investor); + } + }; + fetchInvestors(); + }, [user]); + + useEffect(() => { + setInvestorFormData(initialInvestorData); + }, [initialInvestorData]); + + const handleInvestorChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setInvestorFormData({ ...investorFormData, [name]: value }); + }; + const handleInvestorSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + if ( + investorFormData.maximumInvestment === "" || + investorFormData.minimumInvestment === "" || + investorFormData.investmentInterests?.length === 0 + ) { + alert("Please input the rquired data..."); + return; + } + + if (!checkIfSettingPage) { + const userInfoString = localStorage.getItem("userInfo"); + if (!userInfoString) { + return; + } + try { + const parsed = JSON.parse(userInfoString) as User; + const { name, email, password, role } = parsed; + const userId = await register(name, email, password, role); + if (userId) { + await updateInvestorData({ ...investorFormData, userId: userId }); + + const message = ` +

Hello,

+

Your account is currently under review by our administrators. You will be notified about your account activation within 24 hours.

+

If you have any questions, reply to this email or contact support at trustbridgeai@gmail.

+

Thank you for your patience.

+

Regards
TrustBridgeAi Support Team

+ `; + const sub = "Under Review Account Activation "; + sendMailToUser(message, sub, email); + + navigate("/", { replace: true }); + } + } catch (e) { + console.error("Failed to parse userInfo from localStorage", e); + } finally { + localStorage.removeItem("userInfo"); + } + } else { + updateInvestorData(investorFormData); + const { + investmentInterests, + minimumInvestment, + maximumInvestment, + investmentCriteria, + successfullExits, + minTimline, + maxTimline, + } = investorFormData; + setInvestor({ + ...investor, + investmentInterests, + minimumInvestment, + maximumInvestment, + investmentCriteria, + successfullExits, + minTimline, + maxTimline, + } as Investor); + setInvestorFormData(initialInvestorData); + } + } catch (error) { + console.error(error); + } + }; + + const handleInterests = (e) => { + e.preventDefault(); + const updatedInterests = [ + ...investorFormData.investmentInterests, + investorFormData.interest, + ]; + setInvestorFormData({ + ...investorFormData, + investmentInterests: updatedInterests, + interest: "", + }); + }; + + const handleCriteria = (e) => { + e.preventDefault(); + const updateCriteria = [ + ...investorFormData.investmentCriteria, + investorFormData.criteria, + ]; + setInvestorFormData({ + ...investorFormData, + investmentCriteria: updateCriteria, + criteria: "", + }); + }; + return ( +
+ + Fill the Details as an Investor... + +
+
+ + +
+
+ {investorFormData.investmentInterests?.map((item, idx) => ( +
+ {item} + +
+ ))} +
+
+ = 5} + helperText={`${ + 5 - investorFormData.investmentCriteria.length + } Investment Criteria rules can be added..`} + fullWidth + /> + +
+
+ {investorFormData.investmentCriteria?.map((item, idx) => ( +
+ {item} + +
+ ))} +
+ +
+
+ + + +
+
+ + +
+
+
+ +
+
+
+ ); +}; diff --git a/src/components/settings/ProfileSettings.tsx b/src/components/settings/ProfileSettings.tsx new file mode 100644 index 000000000..c264c0e65 --- /dev/null +++ b/src/components/settings/ProfileSettings.tsx @@ -0,0 +1,155 @@ +import React, { useState } from "react"; +import { Card, CardHeader, CardBody } from "../ui/Card"; +import { Input } from "../ui/Input"; +import { Button } from "../ui/Button"; +import { Avatar } from "../ui/Avatar"; +import { useAuth } from "../../context/AuthContext"; +import { Entrepreneur, Investor, UserRole } from "../../types"; +import { InvestorSettings } from "./InvestorSettings"; +import { EntrepreneurSettings } from "./EntrepreneurSettings"; + +interface ProfileSettingsProps { + user?: Entrepreneur | Investor; + currentUser: any; +} + +export const ProfileSettings: React.FC = ({ + user, + currentUser, +}) => { + const { updateProfile } = useAuth(); + + type UserDetails = { + name?: string; + email?: string; + role?: UserRole | undefined; + bio: string; + location: string; + avatarUrl?: string | File | null; + }; + + const initialValues: UserDetails = { + name: currentUser?.name, + email: currentUser?.email, + role: currentUser?.role, + bio: currentUser?.bio || "", + location: currentUser?.location || "", + avatarUrl: currentUser?.avatarUrl || "", + }; + + const [userDetails, setUserDetails] = useState(initialValues); + const [isFileUploaded, setIsFileUploaded] = useState(false); + + const handleChange = ( + e: React.ChangeEvent + ) => { + const target = e.target as HTMLInputElement & { files?: FileList }; + const { name, value, files } = target; + if (name === "avatarUrl") { + const file = files?.[0] ?? null; + setUserDetails((prev) => ({ ...prev, avatarUrl: file })); + setIsFileUploaded(!!file); + } else { + setUserDetails((prev) => ({ ...prev, [name]: value } as UserDetails)); + } + }; + + const handleSubmit = async (e: React.MouseEvent) => { + e.preventDefault(); + if (!user) return; + updateProfile(user.userId, userDetails); + }; + + return ( +
+ {/* Profile Settings */} + + +

+ Profile Settings +

+
+ +
+ + +
+ + + + +

+ JPG, GIF or PNG. Max size of 800K +

+
+
+ +
+ + + + + + +
+ +
+ + +
+ +
+ +
+
+
+ + {/* Update profile */} + {currentUser?.role !== "admin" && ( + + +

+ Profile Details +

+
+ + {currentUser?.role === "investor" ? ( + + ) : ( + + )} + +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/components/settings/SecuritySettings.tsx b/src/components/settings/SecuritySettings.tsx new file mode 100644 index 000000000..67b2d75e7 --- /dev/null +++ b/src/components/settings/SecuritySettings.tsx @@ -0,0 +1,327 @@ +import React, { useState } from "react"; +import { Card, CardHeader, CardBody } from "../ui/Card"; +import { Input } from "../ui/Input"; +import { Button } from "../ui/Button"; +import { Badge } from "../ui/Badge"; +import { Shield, Smartphone, LogOut, AlertCircle } from "lucide-react"; + +interface Session { + id: string; + device: string; + browser: string; + location: string; + lastActive: string; + isCurrent: boolean; +} + +export const SecuritySettings: React.FC = () => { + const [passwords, setPasswords] = useState({ + current: "", + new: "", + confirm: "", + }); + + const [passwordError, setPasswordError] = useState(""); + const [passwordSuccess, setPasswordSuccess] = useState(false); + const [isUpdatingPassword, setIsUpdatingPassword] = useState(false); + + const [twoFactorEnabled, setTwoFactorEnabled] = useState(false); + const [showTwoFactorSetup, setShowTwoFactorSetup] = useState(false); + const [verificationCode, setVerificationCode] = useState(""); + + const [sessions, setSessions] = useState([ + { + id: "1", + device: "Windows PC", + browser: "Chrome 119", + location: "Lahore, Punjab, PK", + lastActive: "Active now", + isCurrent: true, + }, + { + id: "2", + device: "iPhone 15", + browser: "Safari", + location: "Lahore, Punjab, PK", + lastActive: "2 hours ago", + isCurrent: false, + }, + { + id: "3", + device: "MacBook Pro", + browser: "Chrome 118", + location: "Karachi, Sindh, PK", + lastActive: "Yesterday", + isCurrent: false, + }, + ]); + + const handlePasswordChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setPasswords((prev) => ({ ...prev, [name]: value })); + setPasswordError(""); + setPasswordSuccess(false); + }; + + const validatePassword = (): boolean => { + if (!passwords.current || !passwords.new || !passwords.confirm) { + setPasswordError("All password fields are required"); + return false; + } + + if (passwords.new.length < 8) { + setPasswordError("New password must be at least 8 characters"); + return false; + } + + if (passwords.new !== passwords.confirm) { + setPasswordError("New passwords do not match"); + return false; + } + + if (passwords.current === passwords.new) { + setPasswordError("New password must be different from current password"); + return false; + } + + return true; + }; + + const handlePasswordUpdate = async () => { + if (!validatePassword()) return; + + setIsUpdatingPassword(true); + setPasswordError(""); + + try { + // Simulate API call + await new Promise((resolve) => setTimeout(resolve, 1500)); + + // TODO: Replace with actual API call + // await updatePassword(passwords.current, passwords.new); + + setPasswordSuccess(true); + setPasswords({ current: "", new: "", confirm: "" }); + + setTimeout(() => setPasswordSuccess(false), 5000); + } catch (error) { + setPasswordError("Failed to update password. Please try again."); + } finally { + setIsUpdatingPassword(false); + } + }; + + const handleToggleTwoFactor = () => { + if (twoFactorEnabled) { + // Disable 2FA + setTwoFactorEnabled(false); + setShowTwoFactorSetup(false); + } else { + // Show 2FA setup + setShowTwoFactorSetup(true); + } + }; + + const handleVerifyTwoFactor = async () => { + if (verificationCode.length !== 6) { + return; + } + + try { + // Simulate API call + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // TODO: Replace with actual API call + // await verifyTwoFactorCode(verificationCode); + + setTwoFactorEnabled(true); + setShowTwoFactorSetup(false); + setVerificationCode(""); + } catch (error) { + console.error("Failed to verify 2FA code"); + } + }; + + const handleRevokeSession = (sessionId: string) => { + setSessions((prev) => prev.filter((session) => session.id !== sessionId)); + }; + + const handleRevokeAllSessions = () => { + setSessions((prev) => prev.filter((session) => session.isCurrent)); + }; + + return ( +
+ {/* Two-Factor Authentication */} + + +
+ +

+ Two-Factor Authentication +

+
+
+ +
+
+

+ Add an extra layer of security to your account by requiring a + verification code in addition to your password. +

+ + {twoFactorEnabled ? "Enabled" : "Not Enabled"} + +
+ +
+ + {showTwoFactorSetup && !twoFactorEnabled && ( +
+
+ +
+

+ Set Up Two-Factor Authentication +

+
    +
  1. 1. Download an authenticator app (Google Authenticator, Authy, etc.)
  2. +
  3. 2. Scan this QR code with your authenticator app
  4. +
  5. 3. Enter the 6-digit code below to verify
  6. +
+ + {/* Mock QR Code */} +
+
+ QR Code +
+ Placeholder +
+
+ +
+ setVerificationCode(e.target.value)} + maxLength={6} + /> +
+ + +
+
+
+
+
+ )} +
+
+ + {/* Change Password */} + + +

Change Password

+
+ + {passwordError && ( +
+ +

{passwordError}

+
+ )} + + {passwordSuccess && ( +
+ +

+ Password updated successfully! +

+
+ )} + + + + + + + +
+ +
+
+
+ + {/* Login History */} + + +

+ Recent Login Activity +

+
+ +
+ {[ + { date: "Nov 26, 2025 09:45 AM", location: "Lahore, PK", status: "success" }, + { date: "Nov 25, 2025 02:30 PM", location: "Lahore, PK", status: "success" }, + { date: "Nov 24, 2025 11:20 AM", location: "Karachi, PK", status: "success" }, + { date: "Nov 23, 2025 08:15 PM", location: "Unknown", status: "failed" }, + ].map((activity, index) => ( +
+
+

{activity.location}

+

{activity.date}

+
+ + {activity.status === "success" ? "Success" : "Failed"} + +
+ ))} +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/ui/Avatar.tsx b/src/components/ui/Avatar.tsx index fb0ab2ec7..482910188 100644 --- a/src/components/ui/Avatar.tsx +++ b/src/components/ui/Avatar.tsx @@ -1,45 +1,43 @@ -import React from 'react'; +import React from "react"; -export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; +export type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl"; interface AvatarProps { - src: string; - alt: string; + src: string | File | null; + alt: string | undefined; size?: AvatarSize; className?: string; - status?: 'online' | 'offline' | 'away' | 'busy'; + status?: "online" | "offline"; } export const Avatar: React.FC = ({ src, alt, - size = 'md', - className = '', + size = "md", + className = "", status, }) => { const sizeClasses = { - xs: 'h-6 w-6', - sm: 'h-8 w-8', - md: 'h-10 w-10', - lg: 'h-12 w-12', - xl: 'h-16 w-16', + xs: "h-6 w-6", + sm: "h-8 w-8", + md: "h-10 w-10", + lg: "h-12 w-12", + xl: "h-16 w-16", }; - + const statusColors = { - online: 'bg-success-500', - offline: 'bg-gray-400', - away: 'bg-warning-500', - busy: 'bg-error-500', + online: "bg-success-500", + offline: "bg-gray-400", }; - + const statusSizes = { - xs: 'h-1.5 w-1.5', - sm: 'h-2 w-2', - md: 'h-2.5 w-2.5', - lg: 'h-3 w-3', - xl: 'h-4 w-4', + xs: "h-1.5 w-1.5", + sm: "h-2 w-2", + md: "h-2.5 w-2.5", + lg: "h-3 w-3", + xl: "h-4 w-4", }; - + return (
= ({ // Fallback to initials if image fails to load const target = e.target as HTMLImageElement; target.onerror = null; - target.src = `https://ui-avatars.com/api/?name=${encodeURIComponent(alt)}&background=random`; + target.src = `https://ui-avatars.com/api/?name=${encodeURIComponent( + alt + )}&background=random`; }} /> - + {status && ( - )}
); -}; \ No newline at end of file +}; diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx index 8c84dc3ed..6fd01081b 100644 --- a/src/components/ui/Badge.tsx +++ b/src/components/ui/Badge.tsx @@ -9,6 +9,7 @@ interface BadgeProps { size?: BadgeSize; rounded?: boolean; className?: string; + onClick?:EventListener; } export const Badge: React.FC = ({ @@ -17,6 +18,7 @@ export const Badge: React.FC = ({ size = 'md', rounded = false, className = '', + onClick, }) => { const variantClasses = { primary: 'bg-primary-100 text-primary-800', @@ -39,6 +41,7 @@ export const Badge: React.FC = ({ return ( {children} diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 95cab08ef..baf6d6ee8 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -25,7 +25,7 @@ export const Button: React.FC = ({ ...props }) => { // Base styles - const baseStyles = 'inline-flex items-center justify-center font-medium rounded-md transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2'; + const baseStyles = 'inline-flex items-center justify-center font-medium rounded-md transition-colors duration-200 focus:outline-none'; // Size styles const sizeStyles = { @@ -42,10 +42,10 @@ export const Button: React.FC = ({ secondary: 'bg-secondary-600 text-white hover:bg-secondary-700 focus:ring-secondary-500', accent: 'bg-accent-500 text-white hover:bg-accent-600 focus:ring-accent-400', outline: 'border border-gray-300 bg-transparent text-gray-700 hover:bg-gray-50 focus:ring-primary-500', - ghost: 'bg-transparent hover:bg-gray-100 text-gray-700 focus:ring-primary-500', + ghost: 'bg-transparent hover:bg-gray-100 text-gray-700 focus:bg-gray-200', link: 'bg-transparent text-primary-600 hover:text-primary-700 hover:underline focus:ring-primary-500 p-0', - success: 'bg-success-500 text-white hover:bg-success-700 focus:ring-success-500', - warning: 'bg-warning-500 text-white hover:bg-warning-700 focus:ring-warning-500', + success: 'bg-success-500 text-white hover:bg-transparent hover:text-success-500 hover:outline focus:bg-success-700 focus:text-white', + warning: 'bg-transparent text-yellow-700 hover:outline focus:bg-warning-700 focus:text-white', error: 'bg-error-500 text-white hover:bg-error-700 focus:ring-error-500', }; diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx index 418ba0756..469cf7490 100644 --- a/src/components/ui/Card.tsx +++ b/src/components/ui/Card.tsx @@ -5,6 +5,7 @@ interface CardProps { className?: string; onClick?: () => void; hoverable?: boolean; + setBanner?:boolean; } export const Card: React.FC = ({ @@ -12,6 +13,7 @@ export const Card: React.FC = ({ className = '', onClick, hoverable = false, + setBanner = false }) => { const hoverableClass = hoverable ? 'transform hover:-translate-y-1 transition-transform duration-300 cursor-pointer' : ''; const clickableClass = onClick ? 'cursor-pointer' : ''; @@ -21,6 +23,9 @@ export const Card: React.FC = ({ className={`bg-white rounded-lg shadow-md overflow-hidden ${hoverableClass} ${clickableClass} ${className}`} onClick={onClick} > +
+

Entrepreneur not set his profile yet..

+
{children} ); diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx index 2e16a6973..d12f46b49 100644 --- a/src/components/ui/Input.tsx +++ b/src/components/ui/Input.tsx @@ -1,66 +1,82 @@ -import React, { forwardRef } from 'react'; +import React, { forwardRef } from "react"; -export interface InputProps extends React.InputHTMLAttributes { +export interface InputProps + extends React.InputHTMLAttributes { label?: string; error?: string; helperText?: string; + value: string | number; startAdornment?: React.ReactNode; endAdornment?: React.ReactNode; fullWidth?: boolean; } -export const Input = forwardRef(({ - label, - error, - helperText, - startAdornment, - endAdornment, - fullWidth = false, - className = '', - ...props -}, ref) => { - - const widthClass = fullWidth ? 'w-full' : ''; - const errorClass = error ? 'border-error-500 focus:border-error-500 focus:ring-error-500' : 'border-gray-300 focus:border-primary-500 focus:ring-primary-500'; - - const inputBaseClass = `block rounded-md shadow-sm focus:ring-2 focus:ring-opacity-50 sm:text-sm ${errorClass}`; - const adornmentClass = startAdornment ? 'pl-10' : ''; - - return ( -
- {label && ( - - )} - -
- {startAdornment && ( -
- {startAdornment} -
+export const Input = forwardRef( + ( + { + label, + error, + helperText, + startAdornment, + endAdornment, + value, + fullWidth = false, + className = "", + onChange, + ...props + }, + ref + ) => { + const widthClass = fullWidth ? "w-full" : ""; + const errorClass = error + ? "border-error-500 focus:border-error-500 focus:ring-error-500" + : "border-gray-300 focus:border-gray-700"; + + const inputBaseClass = `block rounded-md shadow-sm sm:text-sm ${errorClass}`; + const adornmentClass = startAdornment ? "pl-10" : ""; + + return ( +
+ {label && ( + )} - - - - {endAdornment && ( -
- {endAdornment} -
+ +
+ {startAdornment && ( +
+ {startAdornment} +
+ )} + + + + {endAdornment && ( +
+ {endAdornment} +
+ )} +
+ + {(error || helperText) && ( +

+ {error || helperText} +

)}
- - {(error || helperText) && ( -

- {error || helperText} -

- )} -
- ); -}); + ); + } +); -Input.displayName = 'Input'; \ No newline at end of file +Input.displayName = "Input"; diff --git a/src/components/ui/ThreeDotsButton.tsx b/src/components/ui/ThreeDotsButton.tsx new file mode 100644 index 000000000..64fd46ae3 --- /dev/null +++ b/src/components/ui/ThreeDotsButton.tsx @@ -0,0 +1,130 @@ +import React from "react"; + +export type ButtonVariant = + | "primary" + | "secondary" + | "accent" + | "outline" + | "ghost" + | "link" + | "success" + | "warning" + | "error"; +export type ButtonSize = "xs" | "sm" | "md" | "lg" | "xl"; + +interface ButtonProps extends React.ButtonHTMLAttributes { + variant?: ButtonVariant; + size?: ButtonSize; + fullWidth?: boolean; + isLoading?: boolean; + leftIcon?: React.ReactNode; + rightIcon?: React.ReactNode; +} + +export const ThreeDotsButton: React.FC = ({ + variant = "primary", + size = "md", + fullWidth = false, + isLoading = false, + leftIcon, + rightIcon, + className = "", + disabled, + ...props +}) => { + // Base styles + const baseStyles = + "inline-flex items-center justify-center font-medium rounded-md transition-colors duration-200 focus:outline-none focus:ring-offset-2"; + + // Size styles + const sizeStyles = { + xs: "text-xs px-2 py-1", + sm: "text-sm px-3 py-1.5", + md: "text-sm px-4 py-2", + lg: "text-base px-5 py-2.5", + xl: "text-lg px-6 py-3", + }; + + // Variant styles + const variantStyles = { + primary: + "bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500", + secondary: + "bg-secondary-600 text-white hover:bg-secondary-700 focus:ring-secondary-500", + accent: + "bg-accent-500 text-white hover:bg-accent-600 focus:ring-accent-400", + outline: + "border border-gray-300 bg-transparent text-gray-700 hover:bg-gray-50 focus:ring-primary-500", + ghost: + "bg-transparent hover:bg-gray-300 transition-all duration-300 text-gray-700", + link: "bg-transparent hover:text-primary-700 hover:underline focus:ring-primary-500 p-0", + success: + "bg-success-500 text-white hover:bg-success-700 focus:ring-success-500", + warning: + "bg-warning-500 text-white hover:bg-warning-700 focus:ring-warning-500", + error: "bg-error-500 text-white hover:bg-error-700 focus:ring-error-500", + }; + + // Loading state + const loadingClass = isLoading ? "opacity-70 cursor-not-allowed" : ""; + + // Width + const widthClass = fullWidth ? "w-full" : ""; + + // Disabled state + const disabledClass = disabled + ? "opacity-50 cursor-not-allowed pointer-events-none" + : ""; + + const combinedClassName = `${baseStyles} ${sizeStyles[size]} ${variantStyles[variant]} ${widthClass} ${loadingClass} ${disabledClass} ${className}`; + + return ( + + ); +}; diff --git a/src/components/user/UserDetails.tsx b/src/components/user/UserDetails.tsx new file mode 100644 index 000000000..9d1f639ad --- /dev/null +++ b/src/components/user/UserDetails.tsx @@ -0,0 +1,48 @@ +import React, { useEffect, useState } from "react"; +import { InvestorSettings } from "../settings/InvestorSettings"; +import { EntrepreneurSettings } from "../settings/EntrepreneurSettings"; +import { useAuth } from "../../context/AuthContext"; +import { Card } from "../ui/Card"; + +export const UserDetails: React.FC = () => { + const [role, setRole] = useState(null); + const { logout } = useAuth(); + + useEffect(() => { + const userInfoString = localStorage.getItem("userInfo"); + if (!userInfoString) { + setRole(null); + return; + } + try { + const parsed = JSON.parse(userInfoString); + setRole(parsed.role); + } catch (e) { + console.error("Failed to parse userInfo from localStorage", e); + setRole(null); + } + }, []); + + return ( + <> +
+ {/* background */} +
+
+
+ + {role === "investor" ? ( + + ) : ( + + )} + +
+
+ + ); +}; diff --git a/src/components/webrtc/IncomingCallModal.tsx b/src/components/webrtc/IncomingCallModal.tsx new file mode 100644 index 000000000..f2ddec55a --- /dev/null +++ b/src/components/webrtc/IncomingCallModal.tsx @@ -0,0 +1,36 @@ +// IncomingCallModal.tsx +import React from "react"; + +type Props = { + callType:string; + fromName:string; + onAccept: () => void; + onReject: () => void; +}; + +const IncomingCallModal: React.FC = ({ callType,fromName, onAccept, onReject }) => { + return ( +
+
+

📞 Incoming Call

+

{fromName} request you for {callType} call

+
+ + +
+
+
+ ); +}; + +export default IncomingCallModal; diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 5d07bcf0a..76d0be0da 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -1,47 +1,105 @@ -import React, { createContext, useState, useContext, useEffect } from 'react'; -import { User, UserRole, AuthContextType } from '../types'; -import { users } from '../data/users'; -import toast from 'react-hot-toast'; +import React, { createContext, useState, useContext, useEffect } from "react"; +import { User, UserRole, AuthContextType } from "../types"; +import toast from "react-hot-toast"; +import axios from "axios"; // Create Auth Context const AuthContext = createContext(undefined); // Local storage keys -const USER_STORAGE_KEY = 'business_nexus_user'; -const RESET_TOKEN_KEY = 'business_nexus_reset_token'; - -// Auth Provider Component -export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { +const USER_STORAGE_KEY = "business_nexus_user"; +const RESET_TOKEN_KEY = "business_nexus_reset_token"; +const URL = import.meta.env.VITE_BACKEND_URL; +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); // Check for stored user on initial load useEffect(() => { - const storedUser = localStorage.getItem(USER_STORAGE_KEY); - if (storedUser) { - setUser(JSON.parse(storedUser)); + setIsLoading(true); + const token = localStorage.getItem("token"); + if (token) { + axios + .get(URL + "/auth/verify", { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((res) => { + const { user } = res.data; + if (!(user.exp * 1000 > Date.now())) { + localStorage.removeItem("token"); + } + }) + .catch(() => { + localStorage.removeItem("token"); + setUser(null); + }) + .finally(() => setIsLoading(false)); + } else { + setIsLoading(false); } - setIsLoading(false); }, []); // Mock login function - in a real app, this would make an API call - const login = async (email: string, password: string, role: UserRole): Promise => { + const login = async ( + email: string, + password: string, + role: UserRole + ): Promise => { setIsLoading(true); - + try { // Simulate API call delay - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Find user with matching email and role - const foundUser = users.find(u => u.email === email && u.role === role); - - if (foundUser) { - setUser(foundUser); - localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(foundUser)); - toast.success('Successfully logged in!'); - } else { - throw new Error('Invalid credentials or user not found'); - } + const res = await axios.post( + `${URL}/auth/login`, + { + email, + password, + role, + }, + { + withCredentials: true, + } + ); + const { token, user } = res.data; + + console.log(user); + localStorage.setItem("token", token); + setUser({ ...user, isOnline: true }); + toast.success("Successfully logged in!"); + } catch (error) { + toast.error((error as Error).message); + throw error; + } finally { + setIsLoading(false); + } + }; + const loginWithOauth = async ( + userToken: string, + role: UserRole + ): Promise => { + setIsLoading(true); + + console.log("hello"); + try { + const res = await axios.post( + `${URL}/auth/login-with-oauth`, + { + userToken, + role, + }, + { + headers: { "Content-Type": "application/json" }, + withCredentials: true, + } + ); + const { token, user } = res.data; + + console.log(user); + localStorage.setItem("token", token); + setUser({ ...user, isOnline: true }); + toast.success("Successfully logged in!"); } catch (error) { toast.error((error as Error).message); throw error; @@ -51,62 +109,79 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }; // Mock register function - in a real app, this would make an API call - const register = async (name: string, email: string, password: string, role: UserRole): Promise => { + const register = async ( + name: string, + email: string, + password: string, + role: UserRole + ): Promise => { setIsLoading(true); - + try { // Simulate API call delay - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Check if email already exists - if (users.some(u => u.email === email)) { - throw new Error('Email already in use'); + const res = await axios.post( + `${URL}/auth/register`, + { + name, + email, + password, + role, + }, + { + withCredentials: true, + } + ); + if (res.status === 201) { + toast.success("Account created successfully!"); + const { token, user } = res.data; + localStorage.setItem("token", token); + setUser(user); + return user.userId; } - - // Create new user - const newUser: User = { - id: `${role[0]}${users.length + 1}`, - name, - email, - role, - avatarUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random`, - bio: '', - isOnline: true, - createdAt: new Date().toISOString() - }; - - // Add user to mock data - users.push(newUser); - - setUser(newUser); - localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(newUser)); - toast.success('Account created successfully!'); + else + return null; } catch (error) { toast.error((error as Error).message); - throw error; + return null; } finally { setIsLoading(false); } }; - // Mock forgot password function - const forgotPassword = async (email: string): Promise => { + // Forgot password function + const forgotPassword = async (email: string, role: string): Promise => { try { - // Simulate API call delay - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Check if user exists - const user = users.find(u => u.email === email); - if (!user) { - throw new Error('No account found with this email'); - } - // Generate reset token (in a real app, this would be a secure token) const resetToken = Math.random().toString(36).substring(2, 15); localStorage.setItem(RESET_TOKEN_KEY, resetToken); - - // In a real app, this would send an email - toast.success('Password reset instructions sent to your email'); + + // This would send an email + const resetLink = `https://nexus-gso984fz0-danish-ajmals-projects.vercel.app/reset-password?token=${resetToken}`; + const message = ` +

To reset your password, please click the button below:

+ + Reset Password + +

If the button doesn’t work, copy and paste this link into your browser:

+

${resetLink}

+ `; + + const sub = "Password reset instructions"; + const res = await axios.post( + `${URL}/auth/send-mail`, + { email, message, sub, role }, + { + withCredentials: true, + } + ); + const { user } = res.data; + + localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user)); + toast.success("Password reset instructions sent to your email"); } catch (error) { toast.error((error as Error).message); throw error; @@ -114,20 +189,29 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }; // Mock reset password function - const resetPassword = async (token: string, newPassword: string): Promise => { + const resetPassword = async ( + token: string, + newPassword: string + ): Promise => { try { - // Simulate API call delay - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Verify token const storedToken = localStorage.getItem(RESET_TOKEN_KEY); if (token !== storedToken) { - throw new Error('Invalid or expired reset token'); + throw new Error("Invalid or expired reset token"); } - - // In a real app, this would update the user's password in the database + const savedUser = localStorage.getItem(USER_STORAGE_KEY); + + const res = await axios.patch( + `${URL}/auth/update-password/${savedUser?.userId}`, + { newPassword }, + { withCredentials: true } + ); + const { user } = res.data; + setUser(user); + + // This would update the user's password in the database localStorage.removeItem(RESET_TOKEN_KEY); - toast.success('Password reset successfully'); + localStorage.removeItem("user"); + toast.success("Password reset successfully"); } catch (error) { toast.error((error as Error).message); throw error; @@ -137,48 +221,56 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children // Logout function const logout = (): void => { setUser(null); - localStorage.removeItem(USER_STORAGE_KEY); - toast.success('Logged out successfully'); + localStorage.removeItem("token"); + toast.success("Logged out successfully"); }; // Update user profile - const updateProfile = async (userId: string, updates: Partial): Promise => { - try { - // Simulate API call delay - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Update user in mock data - const userIndex = users.findIndex(u => u.id === userId); - if (userIndex === -1) { - throw new Error('User not found'); - } - - const updatedUser = { ...users[userIndex], ...updates }; - users[userIndex] = updatedUser; - - // Update current user if it's the same user - if (user?.id === userId) { - setUser(updatedUser); - localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(updatedUser)); - } - - toast.success('Profile updated successfully'); - } catch (error) { - toast.error((error as Error).message); - throw error; + const updateProfile = async ( + userId: string |undefined, + userData: User + ): Promise => { + if ( + userData.location === "" && + userData.bio === "" && + userData.avatarUrl === "" + ) { + alert("Make changes to update profile.."); + return; } + + const formData = new FormData(); + formData.append("name", userData.name); + formData.append("location", userData.location); + formData.append("email", userData.email); + formData.append("bio", userData.bio); + formData.append("avatarUrl", userData.avatarUrl); + + await axios + .post(`${URL}/user/update-profile/${userId}`, formData, { + withCredentials: true, + }) + .then((res) => { + toast.success("profile updated successfully."); + const { user } = res.data; + setUser(user); + }) + .catch((err) => { + console.log(err); + }); }; const value = { user, login, + loginWithOauth, register, logout, forgotPassword, resetPassword, updateProfile, isAuthenticated: !!user, - isLoading + isLoading, }; return {children}; @@ -188,7 +280,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children export const useAuth = (): AuthContextType => { const context = useContext(AuthContext); if (context === undefined) { - throw new Error('useAuth must be used within an AuthProvider'); + throw new Error("useAuth must be used within an AuthProvider"); } return context; -}; \ No newline at end of file +}; diff --git a/src/context/SocketContext.tsx b/src/context/SocketContext.tsx new file mode 100644 index 000000000..00f062c87 --- /dev/null +++ b/src/context/SocketContext.tsx @@ -0,0 +1,50 @@ +// SocketContext.tsx +import React, { createContext, useContext, useEffect, useState } from "react"; +import { io, Socket } from "socket.io-client"; +import { useAuth } from "./AuthContext"; // your existing auth context + +type SocketContextType = { + socket: Socket | null; +}; + +const SocketContext = createContext({ socket: null }); + +export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const { user } = useAuth(); // logged in user + const [socket, setSocket] = useState(null); + + useEffect(() => { + if (user) { + // connect socket after login + // const s = io(import.meta.env.VITE_BACKEND_URL, { + // withCredentials: true, + // transports: ["websocket", "polling"], + // }); + + // s.on("connect", () => { + // console.log("Connected to socket:", s.id); + // // tell backend this userId is online + // s.emit("join", user.userId); + // }); + + // setSocket(s); + + // cleanup on unmount or logout + return () => { + // s.disconnect(); + setSocket(null); + console.log(" Socket disconnected"); + }; + } + }, [user]); + + return ( + + {children} + + ); +}; +export const useSocket = () => useContext(SocketContext); + diff --git a/src/data/collaborationRequests.ts b/src/data/collaborationRequests.ts index 092488f9c..a5a0eb641 100644 --- a/src/data/collaborationRequests.ts +++ b/src/data/collaborationRequests.ts @@ -1,90 +1,92 @@ -import { CollaborationRequest } from '../types'; - -export const collaborationRequests: CollaborationRequest[] = [ - { - id: 'req1', - investorId: 'i1', - entrepreneurId: 'e1', - message: 'Id like to explore potential investment in TechWave AI. Your AI-driven financial analytics platform aligns well with my investment thesis.', - status: 'pending', - createdAt: '2023-08-10T15:30:00Z' - }, - { - id: 'req2', - investorId: 'i2', - entrepreneurId: 'e1', - message: 'Interested in discussing how TechWave AI can incorporate sustainable practices. Lets connect to explore potential collaboration.', - status: 'accepted', - createdAt: '2023-08-05T11:45:00Z' - }, - { - id: 'req3', - investorId: 'i3', - entrepreneurId: 'e3', - message: 'Your HealthPulse platform addresses a critical need in mental healthcare. Id like to learn more about your traction and roadmap.', - status: 'pending', - createdAt: '2023-08-12T09:20:00Z' - }, - { - id: 'req4', - investorId: 'i2', - entrepreneurId: 'e2', - message: 'GreenLifes biodegradable packaging solutions align with my focus on sustainable investments. Lets discuss scaling possibilities.', - status: 'accepted', - createdAt: '2023-07-28T14:15:00Z' - }, - { - id: 'req5', - investorId: 'i1', - entrepreneurId: 'e4', - message: 'Your UrbanFarm concept is fascinating. Im interested in learning more about your IoT implementation and market validation.', - status: 'rejected', - createdAt: '2023-08-03T16:50:00Z' - } -]; +import axios from "axios"; +import { CollaborationRequest } from "../types"; +import toast from "react-hot-toast"; +const URL = import.meta.env.VITE_BACKEND_URL; // Helper function to get collaboration requests for an entrepreneur -export const getRequestsForEntrepreneur = (entrepreneurId: string): CollaborationRequest[] => { - return collaborationRequests - .filter(request => request.entrepreneurId === entrepreneurId) - .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); +export const getRequestsForEntrepreneur = async ( + enter_id: string +): CollaborationRequest[] => { + const res = await axios.get( + URL + "/requests/get-request-for-enterpreneur/" + enter_id, + { + withCredentials: true, + } + ); + const { requests } = res.data; + return requests; }; // Helper function to get collaboration requests sent by an investor -export const getRequestsFromInvestor = (investorId: string): CollaborationRequest[] => { - return collaborationRequests - .filter(request => request.investorId === investorId) - .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); +export const getRequestsFromInvestor = async ( + inves_id: string +): CollaborationRequest[] => { + const res = await axios.post( + URL + "/requests/get-request-for-investor", + { inves_id }, + { + withCredentials: true, + } + ); + const { requests } = res.data; + return requests; +}; +export const checkRequestsFromInvestor = async ( + inves_id: string | undefined, + enter_id: string | undefined +): Promise => { + try { + const body = { inves_id, enter_id }; + const res = await axios.post( + URL + "/requests/check-request-for-investor", + body, + { withCredentials: true } + ); + + const { request } = res.data; + console.log(request); + return request.requestStatus; // true if found, false otherwise + } catch (error) { + console.error("checkRequestsFromInvestor error:", error); + return "pending"; + } }; // Helper function to update a collaboration request status -export const updateRequestStatus = (requestId: string, newStatus: 'pending' | 'accepted' | 'rejected'): CollaborationRequest | null => { - const requestIndex = collaborationRequests.findIndex(req => req.id === requestId); - if (requestIndex === -1) return null; - - collaborationRequests[requestIndex] = { - ...collaborationRequests[requestIndex], - status: newStatus - }; - - return collaborationRequests[requestIndex]; +export const updateRequestStatus = async ( + requestId: string, + newStatus: "pending" | "accepted" | "rejected" +): CollaborationRequest => { + const res = await axios.put( + URL + "/requests/update-status", + { requestId, newStatus }, + { withCredentials: true } + ); + const { request } = res.data; + toast.success("request status updated"); + return request; }; // Helper function to create a new collaboration request -export const createCollaborationRequest = ( - investorId: string, - entrepreneurId: string, +export const createCollaborationRequest = async ( + inves_id: string, + enter_id: string, message: string ): CollaborationRequest => { - const newRequest: CollaborationRequest = { - id: `req${collaborationRequests.length + 1}`, - investorId, - entrepreneurId, + const newRequest = { + inves_id, + enter_id, message, - status: 'pending', - createdAt: new Date().toISOString() + requestStatus: "pending", }; - - collaborationRequests.push(newRequest); - return newRequest; -}; \ No newline at end of file + try { + const res = await axios.post(URL + "/requests/save-request", newRequest, { + withCredentials: true, + }); + toast.success("Request sent.."); + return res.data.request as CollaborationRequest; + } catch (error) { + console.log(error); + throw error; + } +}; diff --git a/src/data/messages.ts b/src/data/messages.ts index 895d3dcd7..3d5231ab9 100644 --- a/src/data/messages.ts +++ b/src/data/messages.ts @@ -1,139 +1,59 @@ -import { Message, ChatConversation } from '../types'; +import axios from "axios"; +const URL = import.meta.env.VITE_BACKEND_URL; -export const messages: Message[] = [ - // Conversation between Sarah (e1) and Michael (i1) - { - id: 'm1', - senderId: 'e1', - receiverId: 'i1', - content: 'Thanks for connecting. Id love to discuss how our AI platform can revolutionize financial analytics for SMBs.', - timestamp: '2023-08-15T10:15:00Z', - isRead: true - }, - { - id: 'm2', - senderId: 'i1', - receiverId: 'e1', - content: 'Im interested in learning more about your tech stack and ML models. Are you available for a call this week?', - timestamp: '2023-08-15T10:30:00Z', - isRead: true - }, - { - id: 'm3', - senderId: 'e1', - receiverId: 'i1', - content: 'Absolutely! I can walk you through our technology and current traction. How does Thursday at 2pm PT work?', - timestamp: '2023-08-15T10:45:00Z', - isRead: true - }, - { - id: 'm4', - senderId: 'i1', - receiverId: 'e1', - content: 'Thursday works great. Ill send a calendar invite. Looking forward to it!', - timestamp: '2023-08-15T11:00:00Z', - isRead: false - }, - - // Conversation between Maya (e3) and Jennifer (i2) - { - id: 'm5', - senderId: 'i2', - receiverId: 'e3', - content: 'I saw your pitch for HealthPulse and Im intrigued by your approach to mental healthcare accessibility.', - timestamp: '2023-08-16T09:00:00Z', - isRead: true - }, - { - id: 'm6', - senderId: 'e3', - receiverId: 'i2', - content: 'Thank you, Jennifer! Mental health services need to be more accessible, especially in underserved communities.', - timestamp: '2023-08-16T09:15:00Z', - isRead: true - }, - { - id: 'm7', - senderId: 'i2', - receiverId: 'e3', - content: 'I completely agree. Could you share more about your user acquisition strategy and current metrics?', - timestamp: '2023-08-16T09:30:00Z', - isRead: false - }, +// Helper function to get messages between two users +export const getMessagesBetweenUsers = async ( + user1Id: string, + user2Id: string +) => { + const res = await axios.get( + `${URL}/message/get-messages-btw-users?sender=${user1Id}&receiver=${user2Id}`, + { + withCredentials: true, + } + ); + const { messages } = res.data; + return messages; +}; - // Conversation between David (e2) and Robert (i3) - { - id: 'm8', - senderId: 'e2', - receiverId: 'i3', - content: 'Hello Robert, I noticed you invest in healthcare. While GreenLife is focused on sustainable packaging, we have some applications in medical supplies.', - timestamp: '2023-08-17T14:00:00Z', - isRead: true - }, - { - id: 'm9', - senderId: 'i3', - receiverId: 'e2', - content: 'Interesting crossover, David. Id be interested in learning more about your biodegradable materials and how they could be used in healthcare.', - timestamp: '2023-08-17T15:30:00Z', - isRead: true - }, - { - id: 'm10', - senderId: 'e2', - receiverId: 'i3', - content: 'Great! Weve been developing materials that can safely package medical devices while being eco-friendly. Our tests show 40% less environmental impact.', - timestamp: '2023-08-17T16:45:00Z', - isRead: false +// save Messages btw users +export const saveMessagesBetweenUsers = async (newMessage: Any) => { + try { + const res = await axios.post(`${URL}/message/save-message`, newMessage, { + withCredentials: true, + }); + const { message } = res.data; + return message; + } catch (err) { + console.log(err); } -]; - -// Helper function to get messages between two users -export const getMessagesBetweenUsers = (user1Id: string, user2Id: string): Message[] => { - return messages.filter( - message => - (message.senderId === user1Id && message.receiverId === user2Id) || - (message.senderId === user2Id && message.receiverId === user1Id) - ).sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); }; // Helper function to get conversations for a user -export const getConversationsForUser = (userId: string): ChatConversation[] => { +export const getConversationsForUser = async ( + currentUserId: string | undefined +) => { // Get unique conversation partners - const conversationPartners = new Set(); - - messages.forEach(message => { - if (message.senderId === userId) { - conversationPartners.add(message.receiverId); - } - if (message.receiverId === userId) { - conversationPartners.add(message.senderId); + const res = await axios.get( + `${URL}/conversation/get-conversations-for-user?currentUserId=${currentUserId}`, + { + withCredentials: true, } - }); - - // Create conversation objects - return Array.from(conversationPartners).map(partnerId => { - const conversationMessages = getMessagesBetweenUsers(userId, partnerId); - const lastMessage = conversationMessages[conversationMessages.length - 1]; - - return { - id: `conv-${userId}-${partnerId}`, - participants: [userId, partnerId], - lastMessage, - updatedAt: lastMessage?.timestamp || new Date().toISOString() - }; - }).sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); + ); + const { conversation } = res.data; + return conversation; }; -// Helper function to send a new message -export const sendMessage = (newMessage: Omit): Message => { - const message: Message = { - ...newMessage, - id: `m${messages.length + 1}`, - timestamp: new Date().toISOString(), - isRead: false - }; - - messages.push(message); - return message; -}; \ No newline at end of file +// Update conversations dynamically +export const updateConversationsForUser = async (con: object): any => { + // Get unique conversation partners + const res = await axios.post( + `${URL}/conversation/update-conversations-for-user`, + con, + { + withCredentials: true, + } + ); + const { conversationForSender } = res.data; + return conversationForSender; +}; diff --git a/src/data/users.ts b/src/data/users.ts index b46cc84cf..dd401a651 100644 --- a/src/data/users.ts +++ b/src/data/users.ts @@ -1,136 +1,127 @@ -import { Entrepreneur, Investor } from '../types'; +import axios from "axios"; +import toast from "react-hot-toast"; +const URL = import.meta.env.VITE_BACKEND_URL; -export const entrepreneurs: Entrepreneur[] = [ - { - id: 'e1', - name: 'Sarah Johnson', - email: 'sarah@techwave.io', - role: 'entrepreneur', - avatarUrl: 'https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg', - bio: 'Serial entrepreneur with 10+ years of experience in SaaS and fintech.', - startupName: 'TechWave AI', - pitchSummary: 'AI-powered financial analytics platform helping SMBs make data-driven decisions.', - fundingNeeded: '$1.5M', - industry: 'FinTech', - location: 'San Francisco, CA', - foundedYear: 2021, - teamSize: 12, - isOnline: true, - createdAt: '2023-01-15T09:24:00Z' - }, - { - id: 'e2', - name: 'David Chen', - email: 'david@greenlife.co', - role: 'entrepreneur', - avatarUrl: 'https://images.pexels.com/photos/614810/pexels-photo-614810.jpeg', - bio: 'Environmental scientist turned entrepreneur. Passionate about sustainable solutions.', - startupName: 'GreenLife Solutions', - pitchSummary: 'Biodegradable packaging alternatives for consumer goods and food industry.', - fundingNeeded: '$2M', - industry: 'CleanTech', - location: 'Portland, OR', - foundedYear: 2020, - teamSize: 8, - isOnline: false, - createdAt: '2022-03-10T14:35:00Z' - }, - { - id: 'e3', - name: 'Maya Patel', - email: 'maya@healthpulse.com', - role: 'entrepreneur', - avatarUrl: 'https://images.pexels.com/photos/415829/pexels-photo-415829.jpeg', - bio: 'Former healthcare professional with an MBA. Building tech to improve patient care.', - startupName: 'HealthPulse', - pitchSummary: 'Mobile platform connecting patients with mental health professionals in real-time.', - fundingNeeded: '$800K', - industry: 'HealthTech', - location: 'Boston, MA', - foundedYear: 2022, - teamSize: 5, - isOnline: true, - createdAt: '2022-07-22T11:42:00Z' - }, - { - id: 'e4', - name: 'James Wilson', - email: 'james@urbanfarm.io', - role: 'entrepreneur', - avatarUrl: 'https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg', - bio: 'Agricultural engineer focused on urban farming solutions and food security.', - startupName: 'UrbanFarm', - pitchSummary: 'IoT-enabled vertical farming systems for urban environments and food deserts.', - fundingNeeded: '$3M', - industry: 'AgTech', - location: 'Chicago, IL', - foundedYear: 2019, - teamSize: 14, - isOnline: false, - createdAt: '2021-11-05T16:18:00Z' +export const getInvestorsFromDb = async () => { + try { + const res = await axios.get(URL + "/investor/get-investors", { + withCredentials: true, + }); + const { investors } = res.data; + return investors; + } catch (err) { + console.log(err); } -]; +}; + +export const getInvestorById = async (id) => { + try { + const res = await axios.get(URL + "/investor/get-investor-by-id/" + id, { + withCredentials: true, + }); + const { investor } = res.data; + return investor; + } catch (err) { + console.log(err); + } +}; + +export const getEnterprenuerFromDb = async () => { + try { + const res = await axios.get(URL + "/entrepreneur/get-entrepreneurs", { + withCredentials: true, + }); + const { entrepreneurs } = res.data; + return entrepreneurs; + } catch (err) { + console.log(err); + } +}; -export const investors: Investor[] = [ - { - id: 'i1', - name: 'Michael Rodriguez', - email: 'michael@vcinnovate.com', - role: 'investor', - avatarUrl: 'https://images.pexels.com/photos/2379004/pexels-photo-2379004.jpeg', - bio: 'Early-stage investor with focus on B2B SaaS and fintech. Previously founded and exited two startups.', - investmentInterests: ['FinTech', 'SaaS', 'AI/ML'], - investmentStage: ['Seed', 'Series A'], - portfolioCompanies: ['PayStream', 'DataSense', 'CloudSecure'], - totalInvestments: 12, - minimumInvestment: '$250K', - maximumInvestment: '$1.5M', - isOnline: true, - createdAt: '2020-05-18T10:15:00Z' - }, - { - id: 'i2', - name: 'Jennifer Lee', - email: 'jennifer@impactvc.org', - role: 'investor', - avatarUrl: 'https://images.pexels.com/photos/1181686/pexels-photo-1181686.jpeg', - bio: 'Impact investor focused on climate tech, sustainable agriculture, and clean energy.', - investmentInterests: ['CleanTech', 'AgTech', 'Sustainability'], - investmentStage: ['Seed', 'Series A', 'Series B'], - portfolioCompanies: ['SolarFlow', 'EcoPackage', 'CleanWater Solutions'], - totalInvestments: 18, - minimumInvestment: '$500K', - maximumInvestment: '$3M', - isOnline: false, - createdAt: '2019-08-30T15:40:00Z' - }, - { - id: 'i3', - name: 'Robert Torres', - email: 'robert@healthventures.com', - role: 'investor', - avatarUrl: 'https://images.pexels.com/photos/834863/pexels-photo-834863.jpeg', - bio: 'Healthcare-focused investor with medical background. Looking for innovations in patient care and biotech.', - investmentInterests: ['HealthTech', 'BioTech', 'Medical Devices'], - investmentStage: ['Series A', 'Series B'], - portfolioCompanies: ['MediTrack', 'BioGenics', 'Patient+'], - totalInvestments: 9, - minimumInvestment: '$1M', - maximumInvestment: '$5M', - isOnline: true, - createdAt: '2021-02-12T09:30:00Z' +export const getEnterpreneurById = async (id) => { + try { + const res = await axios.get( + URL + "/entrepreneur/get-entrepreneur-by-id/" + id, + { + withCredentials: true, + } + ); + const { entrepreneur } = res.data; + return entrepreneur; + } catch (err) { + console.log(err); } -]; +}; -// Combined user data for lookup -export const users = [...entrepreneurs, ...investors]; +export const updateEntrepreneurData = async (formData: any) => { + try { + await axios.put( + `${URL}/entrepreneur/update-profile/${formData.userId}`, + formData, + { + withCredentials: true, + } + ); + toast.success("User data updated successfully."); + } catch (err) { + console.log(err); + } +}; -// Helper function to find a user by ID -export const findUserById = (id: string) => { - return users.find(user => user.id === id) || null; +export const updateInvestorData = async (formData: any) => { + try { + await axios.put( + `${URL}/investor/update-profile/${formData.userId}`, + formData, + { + withCredentials: true, + } + ); + toast.success("User data updated successfully."); + } catch (err) { + console.log(err); + } }; -// Helper function to get a user by role -export const getUsersByRole = (role: 'entrepreneur' | 'investor') => { - return users.filter(user => user.role === role); -}; \ No newline at end of file +export const sendMailToUser = async (message:string,sub:string,email: string) => { + try { + const res = await axios.post( + `${URL}/auth/send-mail`, + { email, message, sub }, + { + withCredentials: true, + } + ); + + if (res.status === 200) toast.success("User data updated successfully."); + } catch (err) { + console.log(err); + } +}; + +export const getUserFromDb = async (id) => { + try { + const res = await axios.get(`${URL}/user/get-user-by-id/${id}`); + const { user } = res.data; + return user; + } catch (err) { + console.log(err); + } +}; + +export const getSuccessfulEntrepreneurs = () => {}; + +export const AmountMeasureWithTags = (amount: number) => { + if (amount !== 0) { + const val = amount; + + let formattedVal = ""; + formattedVal = Intl.NumberFormat("en-US", { + notation: "compact", + compactDisplay: "short", + }).format(val); + + return formattedVal; + } + return "0"; +}; diff --git a/src/pages/admin/FraudAndRiskDetection.tsx b/src/pages/admin/FraudAndRiskDetection.tsx new file mode 100644 index 000000000..b97b6da85 --- /dev/null +++ b/src/pages/admin/FraudAndRiskDetection.tsx @@ -0,0 +1,3 @@ +export const FraudAndRiskDetection: React.FC = () => { + return

FraudAndRiskDetection page

; +}; diff --git a/src/pages/admin/Users.tsx b/src/pages/admin/Users.tsx new file mode 100644 index 000000000..d7b957e70 --- /dev/null +++ b/src/pages/admin/Users.tsx @@ -0,0 +1,433 @@ +import React, { useEffect, useRef, useState } from "react"; +import toast from "react-hot-toast"; +import { ThreeDotsButton } from "../../components/ui/ThreeDotsButton"; +import { Button } from "../../components/ui/Button"; +import { Input } from "../../components/ui/Input"; +import { SearchIcon, X } from "lucide-react"; +import { User } from "../../types"; +import { formatDistanceToNow } from "date-fns"; +import { StartupIndustryChart } from "../../components/admin/StartupIndustryChart"; +import { StartupGrowthChart } from "../../components/admin/StartupGrowthChart"; +import { Card, CardHeader } from "../../components/ui/Card"; +import { useNavigate } from "react-router-dom"; +import { EntrepreneurProfile } from "../profile/EntrepreneurProfile"; +import { InvestorProfile } from "../profile/InvestorProfile"; + +export const Users: React.FC = () => { + const [users, setUsers] = useState([]); + const [searchedusers, setSearchedUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [showDialog, setShowDialog] = useState(false); + const [index, setIndex] = useState(null); + const [query, setQuery] = useState(""); + const [searched, setSearched] = useState(""); + + const [startupGrowthChartData, setStartupGrowthChartData] = useState([]); + const [industryGrowthChartData, setIndustryGrowthChartData] = useState([]); + + const fetchStartupGrowthChartData = async () => { + const res = await fetch( + "http://localhost:5000/admin/users/users-last-year" + ); + const data = await res.json(); + setStartupGrowthChartData(data); + }; + + const fetchIndustryGrowthChartData = async () => { + const res = await fetch( + "http://localhost:5000/admin/users/startup-by-industry" + ); + const data = await res.json(); + setIndustryGrowthChartData(data); + }; + + useEffect(() => { + fetchStartupGrowthChartData(); + fetchIndustryGrowthChartData(); + }, []); + + const deleteUser = async (id: string) => { + if (!confirm("Are you sure you want to delete this user?")) return; + + try { + const res = await fetch( + `${import.meta.env.VITE_BACKEND_URL}/admin/user/${id}`, + { + method: "DELETE", + } + ); + if (!res.ok) throw new Error("Failed to delete"); + toast.success("User deleted successfully"); + setUsers((prev) => prev.filter((u) => u._id !== id)); + } catch { + toast.error("Error deleting user"); + } + }; + + useEffect(() => { + const fetchUsers = async () => { + try { + setLoading(true); + const res = await fetch( + `${import.meta.env.VITE_BACKEND_URL}/admin/get-users` + ); + const data = await res.json(); + setUsers(data); + } catch (error) { + console.error(error); + toast.error("Failed to load users"); + } finally { + setLoading(false); + } + }; + fetchUsers(); + }, []); + + const dialogRef = useRef(null); + + useEffect(() => { + function handleClickOutside(event) { + if (dialogRef.current && !dialogRef.current.contains(event.target)) { + setShowDialog(false); + setIndex(null); + } + } + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const ActionButtons = () => { + return ( +
+ + +
+ ); + }; + type UserData = { + userId: string; + role: string; + }; + type GraphData = { + type?: "startup" | "industry"; + show: boolean; + }; + const [viewUser, setViewUser] = useState(null); + const [profileModal, setProfileModal] = useState(false); + const [graphViewModal, setGraphViewModal] = useState(null); + + const TableRow = ({ user, idx }) => { + return ( + <> + + + {idx + 1}. +

{user.name}

+ + {user.email} + {user.role} + {user.location} + + + {user.isOnline ? "Online" : "Offline"} + + + + {/* This td holds industry + 3-dots at far right */} + + {/* Industry text */} + + {formatDistanceToNow(user.createdAt)} + + + {/* 3-dots — hidden normally, visible on row hover */} + { + e.stopPropagation(); + setIndex(idx); + setShowDialog((prev) => !prev); + }} + /> + + {showDialog && idx === index && ( +
= users.length - 2 + ? "bottom-full mb-2" + : "top-full mt-2" + } + `} + > + + + +
+ )} + + + + ); + }; + + if (loading) return

Loading users...

; + if (users.length === 0) + return

No users found.

; + + return ( +
+ {/* {profileModal && ( */} +
+
+ setProfileModal(false)} + /> +
+ {viewUser?.role === "investor" ? ( + + ) : ( + + )} +
+
+
+ + {/* GraphViewModal */} +
+
+ setGraphViewModal({ show: false })} + /> +
+ +
+ {graphViewModal?.type === "startup" ? ( + + ) : ( + + )} +
+
+ + {/* ✔️ Users Table */} +

+ Manange Users +

+ +
+
{ + e.preventDefault(); + setSearched(query); + const filterUsers = users.filter((ent) => + ent.name.toLowerCase().includes(query.toLowerCase()) + ); + + if (filterUsers.length !== 0) setSearchedUsers([...filterUsers]); + else setSearchedUsers([]); + }} + > + { + setQuery(e.target.value); + if (e.target.value === "") { + setSearchedUsers([]); + setSearched(""); + } + }} + /> +
+ +
+
+
+ +
+ + +

+ Users Analytics +

+

{ + e.preventDefault(); + setGraphViewModal({ show: true, type: "startup" }); + }} + > + View +

+
+
+
+ +
+
+
+ + + +

+ Startup Industry Analytics +

+

{ + e.preventDefault(); + setGraphViewModal({ show: true, type: "industry" }); + }} + > + View +

+
+
+
+ +
+
+
+
+ +
+ + {searched ? `Results '${searched}' searched count: ` : "Total Users:"}{" "} + + {searched ? searchedusers.length : users.length} +
+ +

+ All Users Details +

+ +
+ + + + + + + + + + + + + {searchedusers.length !== 0 ? ( + searchedusers.map((user, idx) => ( + + )) + ) : searched ? ( +
No records found..
+ ) : users.length !== 0 ? ( + users.map((user, idx) => ) + ) : ( +
No records found..
+ )} +
+
NameEmailRoleLocationIs OnlineWhen Registered
+
+ + {/* ✔️ Pending Users Section */} +
+

+ Pending User Approvals +

+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+ Name + + Business Name + + Action +
John CarterCarter Tech + +
Sarah Khan + SK Enterprises + + +
+
+
+
+ ); +}; diff --git a/src/pages/admin/activities.tsx b/src/pages/admin/activities.tsx new file mode 100644 index 000000000..7de9d9e43 --- /dev/null +++ b/src/pages/admin/activities.tsx @@ -0,0 +1,3 @@ +export const Activities:React.FC = () =>{ + return

User Management Page

; +} diff --git a/src/pages/admin/campaigns.tsx b/src/pages/admin/campaigns.tsx new file mode 100644 index 000000000..11be63736 --- /dev/null +++ b/src/pages/admin/campaigns.tsx @@ -0,0 +1,210 @@ +import React, { useEffect, useState } from "react"; +import axios from "axios"; +import toast from "react-hot-toast"; +import { motion, AnimatePresence } from "framer-motion"; +import CampForm from "../../components/camp/CampForm"; + +const URL = import.meta.env.VITE_BACKEND_URL; +interface Campaign { + _id: string; + title: string; + description: string; + goalAmount: number; + raisedAmount: number; + status: string; + category: string; + startDate: string; + endDate: string; + images?: string[]; +} + +const Campaigns: React.FC = () => { + const [campaigns, setCampaigns] = useState([]); + const [filtered, setFiltered] = useState([]); + const [query, setQuery] = useState(""); + const [showForm, setShowForm] = useState(false); + const scrollRef = useRef(null); + + const fetchCampaigns = async () => { + try { + const res = await axios.get(`${URL}/admin/campaigns`); + setCampaigns(res.data); + setFiltered(res.data); + } catch { + toast.error("⚠️ Failed to fetch campaigns"); + } + }; + + const updateStatus = async (id: string, status: string) => { + try { + await axios.put(`${URL}/admin/campaigns/${id}/status`, { + status, + }); + toast.success("✅ Status updated!"); + fetchCampaigns(); + } catch { + toast.error("⚠️ Failed to update status"); + } + }; + + useEffect(() => { + fetchCampaigns(); + }, []); + + const handleSearch = (e: any) => { + e.preventDefault(); + const q = query.toLowerCase(); + const result = campaigns.filter( + (c) => + c.title.toLowerCase().includes(q) || + c.description.toLowerCase().includes(q) || + c.category.toLowerCase().includes(q) + ); + setFiltered(result); + }; + + const total = filtered.length; + const active = filtered.filter((c) => c.status === "active").length; + const stopped = filtered.filter((c) => c.status === "stopped").length; + + const scrollLeft = () => { + if (scrollRef.current) { + scrollRef.current.scrollBy({ left: -500, behavior: "smooth" }); + } + }; + + const scrollRight = () => { + if (scrollRef.current) { + scrollRef.current.scrollBy({ left: 500, behavior: "smooth" }); + } + }; + + return ( +
+
+

Manage Campaigns

+ +
+ + {/* 🔹 Popup Modal */} + + {showForm && ( + + + {/* ❌ Close Button */} + + + { + fetchCampaigns(); + setShowForm(false); + }} + /> + + + )} + + + {/* 🔹 Campaign List */} +
+ {campaigns.map((c) => ( + + {c.images && c.images.length > 0 && ( +
+ {c.images.map((img, i) => ( + {c.title} + ))} +
+ )} +

+ {c.title} +

+

{c.description}

+

+ 🎯 Goal: ${c.goalAmount} | 💰 Raised:{" "} + ${c.raisedAmount} +

+

+ Status:{" "} + + {c.status} + +

+ +
+

+ Start:{" "} + {new Date(c.startDate).toLocaleDateString()} +

+

+ End:{" "} + {new Date(c.endDate).toLocaleDateString()} +

+
+ +

+ Status:{" "} + + {c.status} + +

+ +
+ {c.status === "active" ? ( + + ) : ( + + )} +
+
+ ))} +
+
+ ); +}; + +export default Campaigns; diff --git a/src/pages/admin/flaggedAccounts.tsx b/src/pages/admin/flaggedAccounts.tsx new file mode 100644 index 000000000..fd8baed43 --- /dev/null +++ b/src/pages/admin/flaggedAccounts.tsx @@ -0,0 +1,81 @@ +"use client"; +import React from "react"; + +interface FlaggedUser { + _id: string; + name: string; + email: string; + role: "entrepreneur" | "investor"; + reason: string; + status: "blocked" | "suspended"; +} + +export const FlaggedAccounts: React.FC = () => { + // DUMMY BLOCKED USERS + const flaggedUsers: FlaggedUser[] = [ + { + _id: "1", + name: "Ahmed Khan", + email: "ahmed@example.com", + role: "entrepreneur", + reason: "Multiple failed verification attempts", + status: "blocked", + }, + { + _id: "2", + name: "Emily Parker", + email: "emily@example.com", + role: "investor", + reason: "Suspicious investment activity", + status: "suspended", + }, + { + _id: "3", + name: "Farhan Ali", + email: "farhan@example.com", + role: "entrepreneur", + reason: "Violation of platform policies", + status: "blocked", + }, + ]; + + return ( +
+

Flagged / Blocked Accounts

+ +
+ + + + + + + + + + + + + {flaggedUsers.map((user) => ( + + + + + + + + ))} + +
NameEmailRoleReasonStatus
{user.name}{user.email}{user.role}{user.reason} + {user.status.toUpperCase()} +
+
+
+ ); +}; diff --git a/src/pages/admin/investors.tsx b/src/pages/admin/investors.tsx new file mode 100644 index 000000000..012d13ed1 --- /dev/null +++ b/src/pages/admin/investors.tsx @@ -0,0 +1,264 @@ +import React, { useEffect, useRef, useState } from "react"; +import toast from "react-hot-toast"; +import { Button } from "../../components/ui/Button"; +import { ThreeDotsButton } from "../../components/ui/ThreeDotsButton"; +import { Input } from "../../components/ui/Input"; +import { SearchIcon } from "lucide-react"; + +interface Investor { + _id: string; + name: string; + email: string; + role: string; + totalInvestments?: number; + successfullExits?: number; +} + +export const Investors: React.FC = () => { + const [investors, setInvestors] = useState([]); + const [searchedInvestors, setSearchedInvestors] = useState([]); + const [loading, setLoading] = useState(true); + + const [showDialog, setShowDialog] = useState(false); + const [index, setIndex] = useState(null); + const [query, setQuery] = useState(""); + const [searched, setSearched] = useState(""); + + // Dummy pending entrepreneurs (NO API TOUCH) + const pendingEntrepreneurs = [ + { _id: "1", name: "John Doe", business: "TechWave Solutions" }, + { _id: "2", name: "Sarah Khan", business: "Green Foods Pvt Ltd" }, + ]; + + // Fetch investors + const fetchInvestors = async () => { + try { + setLoading(true); + const res = await fetch( + `${import.meta.env.VITE_BACKEND_URL}/admin/get-users` + ); + const data = await res.json(); + + const filtered = data.filter((u: User) => u.role === "investor"); + setInvestors(filtered); + } catch (error) { + console.error(error); + toast.error("Failed to load investors"); + } finally { + setLoading(false); + } + }; + + const deleteInvestor = async (id: string) => { + if (!confirm("Are you sure you want to delete this investor?")) return; + + try { + const res = await fetch( + `${import.meta.env.VITE_BACKEND_URL}/admin/user/${id}`, + { + method: "DELETE", + } + ); + + if (!res.ok) throw new Error("Failed to delete"); + toast.success("Investor deleted successfully"); + setInvestors((prev) => prev.filter((i) => i._id !== id)); + } catch (error) { + toast.error("Error deleting investor"); + } + }; + + useEffect(() => { + fetchInvestors(); + }, []); + + const dialogRef = useRef(null); + + useEffect(() => { + function handleClickOutside(event) { + // Close only if click is outside dialog AND outside button + if (dialogRef.current && !dialogRef.current.contains(event.target)) { + setShowDialog(false); + setIndex(null); + } + } + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const TableRow = ({ inv, idx }) => { + return ( + <> + + {inv.name} + {inv.email} + {inv.totalInvestments ?? 0} + + + + {inv.successfullExits ?? 0} + + { + e.stopPropagation(); + setIndex(idx); + setShowDialog((prev) => !prev); + }} + /> + + {showDialog && idx === index && ( +
= investors.length - 2 + ? "bottom-full mb-2" + : "top-full mt-2" + } + `} + > + + +
+ )} + + + + ); + }; + + const ActionButtons = () => { + return ( +
+ + +
+ ); + }; + + if (loading) return

Loading investors...

; + + if (investors.length === 0) + return

No investors found.

; + + return ( +
+

+ Manage Investor +

+ +
+
{ + e.preventDefault(); + setSearched(query); + const filterInvestors = investors.filter((ent) => + ent.name.toLowerCase().includes(query.toLowerCase()) + ); + + if (filterInvestors.length !== 0) + setSearchedInvestors([...filterInvestors]); + else setSearchedInvestors([]); + }} + > + { + setQuery(e.target.value); + if (e.target.value === "") { + setSearchedInvestors([]); + setSearched(""); + } + }} + /> +
+ +
+
+
+
+ + {searched + ? `Results '${searched}' searched count: ` + : "Total Investors:"}{" "} + + {searched ? searchedInvestors.length : investors.length} +
+ + {/* EXISTING TABLE (UNCHANGED) */} +
+ + + + + + + + + + + {searchedInvestors.length !== 0 ? ( + searchedInvestors.map((user, idx) => ( + + )) + ) : searched ? ( +
No records found..
+ ) : investors.length !== 0 ? ( + investors.map((user, idx) => ) + ) : ( +
No records found..
+ )} +
+
NameEmailTotal InvestmentsSuccessful Exits
+
+ + {/* NEW DUMMY TABLE */} +

Pending Investors

+ +
+ + + + + + + + + + {pendingEntrepreneurs.map((user) => ( + + + + + + ))} + +
NameBusiness NameAction
{user.name}{user.business} + +
+
+
+ ); +}; diff --git a/src/pages/admin/supporters.tsx b/src/pages/admin/supporters.tsx new file mode 100644 index 000000000..a9a129d15 --- /dev/null +++ b/src/pages/admin/supporters.tsx @@ -0,0 +1,152 @@ +import React, { useEffect, useState } from "react"; +import { Input } from "../../components/ui/Input"; +import { SearchIcon, Trash } from "lucide-react"; +import { Button } from "../../components/ui/Button"; + +interface Supporter { + _id: string; + name: string; + email: string; + campaign: string; + amount: number; +} + +export const Supporters: React.FC = () => { + const [supporters, setSupportors] = useState([]); + const [searchedSupportors, setSearchedSupportors] = useState([]); + const [query, setQuery] = useState(""); + const [searched, setSearched] = useState(""); + + useEffect(() => { + // DUMMY SUPPORTERS DATA + const dumySupporters: Supporter[] = [ + { + _id: "1", + name: "Ali Raza", + email: "ali@example.com", + campaign: "Eco-Friendly Water Bottles", + amount: 5000, + }, + { + _id: "2", + name: "Jessica Smith", + email: "jessica@example.com", + campaign: "AI Study Planner App", + amount: 12000, + }, + { + _id: "3", + name: "Hassan Khan", + email: "hassan@example.com", + campaign: "Organic Farming Project", + amount: 3000, + }, + ]; + setSupportors(dumySupporters); + }, []); + + const TableRow = ({ sup, idx }) => { + return ( + <> + + {sup.name} + {sup.email} + {sup.campaign} + + + + ${sup.amount.toLocaleString()} + + + + + + ); + }; + + return ( +
+

+ Crowdfund Supporters +

+ +
+
{ + e.preventDefault(); + setSearched(query); + const filterSupportors = supporters.filter((ent) => + ent.name.toLowerCase().includes(query.toLowerCase()) + ); + + if (filterSupportors.length !== 0) + setSearchedSupportors([...filterSupportors]); + else setSearchedSupportors([]); + }} + > + { + setQuery(e.target.value); + if (e.target.value === "") { + setSearchedSupportors([]); + setSearched(""); + } + }} + /> +
+ +
+
+
+
+ {searched ? `Results '${searched}' searched count: ` : "Total Supportors:"} + {searched ? searchedSupportors.length : supporters.length} +
+ +
+ + + + + + + + + + + + {searchedSupportors.length !== 0 ? ( + searchedSupportors.map((user, idx) => ( + + )) + ) : searched ? ( +
No records found..
+ ) : supporters.length !== 0 ? ( + supporters.map((user, idx) => ) + ) : ( +
No records found..
+ )} +
+
NameEmailCampaignAmount Invested
+
+
+ ); +}; diff --git a/src/pages/auth/ForgotPasswordPage.tsx b/src/pages/auth/ForgotPasswordPage.tsx index 14a53a209..5949bcacf 100644 --- a/src/pages/auth/ForgotPasswordPage.tsx +++ b/src/pages/auth/ForgotPasswordPage.tsx @@ -20,7 +20,7 @@ export const ForgotPasswordPage: React.FC = () => { await forgotPassword(email); setIsSubmitted(true); } catch (error) { - // Error is handled by the AuthContext + console.log(error); } finally { setIsLoading(false); } diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index bbd5b08a0..60d46c338 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -1,64 +1,80 @@ -import React, { useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -import { User, CircleDollarSign, Building2, LogIn, AlertCircle } from 'lucide-react'; -import { useAuth } from '../../context/AuthContext'; -import { Button } from '../../components/ui/Button'; -import { Input } from '../../components/ui/Input'; -import { UserRole } from '../../types'; +import React, { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { + User, + CircleDollarSign, + Building2, + Globe, + Linkedin, + Shield, + LogIn, + AlertCircle, +} from "lucide-react"; +import { useAuth } from "../../context/AuthContext"; +import { Button } from "../../components/ui/Button"; +import { Input } from "../../components/ui/Input"; +import { UserRole } from "../../types"; export const LoginPage: React.FC = () => { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [role, setRole] = useState('entrepreneur'); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [role, setRole] = useState("entrepreneur"); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); - + const URL = import.meta.env.VITE_BACKEND_URL; + const { login } = useAuth(); const navigate = useNavigate(); - + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setIsLoading(true); - + try { await login(email, password, role); + // Redirect based on user role - navigate(role === 'entrepreneur' ? '/dashboard/entrepreneur' : '/dashboard/investor'); + if (role === "entrepreneur") navigate("/dashboard/entrepreneur"); + else if (role === "investor") navigate("/dashboard/investor"); + else if (role === "admin") navigate("/dashboard/admin"); } catch (err) { setError((err as Error).message); setIsLoading(false); } }; - // For demo purposes, pre-filled credentials const fillDemoCredentials = (userRole: UserRole) => { - if (userRole === 'entrepreneur') { - setEmail('sarah@techwave.io'); - setPassword('password123'); + if (userRole === "entrepreneur") { + setEmail("en@gmail.com"); + setPassword("123"); + } else if (userRole === "investor") { + setEmail("in@gmail.com"); + setPassword("123"); } else { - setEmail('michael@vcinnovate.com'); - setPassword('password123'); + setEmail("admin@gmail.com"); + setPassword("123"); } setRole(userRole); }; - + + const loginWith = (provider: string) => { + window.location.href = `${URL}/auth/${provider}`; + }; + return (
- - - - +

Sign in to Business Nexus

- Connect with investors and entrepreneurs + Connect with entrepreneurs, investors, and admins

@@ -70,41 +86,57 @@ export const LoginPage: React.FC = () => { {error}
)} - +
-
+
+ {/* Entrepreneur */} - + + {/* Investor */} + + {/* Admin */} +
- + { fullWidth startAdornment={} /> - + { required fullWidth /> - +
{ type="checkbox" className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded" /> -
- + - + + {/* Demo Accounts */}
- Demo Accounts + + Demo Accounts +
- -
+ +
- + + +
- +
@@ -192,15 +241,46 @@ export const LoginPage: React.FC = () => { Or
- + + {/* end */} +

- Don't have an account?{' '} - + Don't have an account?{" "} + Sign up

+ +
+
+
+
+
+ Or +
+
+ +
+ + +
diff --git a/src/pages/auth/LoginWithOAuthPage.tsx b/src/pages/auth/LoginWithOAuthPage.tsx new file mode 100644 index 000000000..886fe4bb6 --- /dev/null +++ b/src/pages/auth/LoginWithOAuthPage.tsx @@ -0,0 +1,116 @@ +import { Building2, CircleDollarSign, LogIn } from "lucide-react"; +import React, { useEffect, useState } from "react"; +import { Button } from "../../components/ui/Button"; +import { useAuth } from "../../context/AuthContext"; +import { useNavigate } from "react-router-dom"; +import { UserRole } from "../../types"; + +export const LoginWithOAuthPage: React.FC = () => { + const [role, setRole] = useState("investor"); + const { isLoading, loginWithOauth } = useAuth(); + const navigate = useNavigate(); + const params = new URLSearchParams(window.location.search); + const token = params.get("token"); + + console.log(token); + useEffect(() => { + if (!token) { + alert("Missing token"); + return; + } + }, [token]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!token) { + alert("Missing token"); + return; + } + // token is guaranteed to be a string here + await loginWithOauth(token, role); + navigate( + role === "entrepreneur" + ? "/dashboard/entrepreneur" + : "/dashboard/investor" + ); + }; + + return ( +
+
+
+
+
+ + + + +
+
+
+ +
+ + + +
+
+ +
+
+
+
+
+ ); +}; diff --git a/src/pages/auth/RegisterPage.tsx b/src/pages/auth/RegisterPage.tsx index e6ff2ab89..9310bcf28 100644 --- a/src/pages/auth/RegisterPage.tsx +++ b/src/pages/auth/RegisterPage.tsx @@ -1,61 +1,78 @@ -import React, { useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -import { User, Mail, Lock, CircleDollarSign, Building2, AlertCircle } from 'lucide-react'; -import { useAuth } from '../../context/AuthContext'; -import { Button } from '../../components/ui/Button'; -import { Input } from '../../components/ui/Input'; -import { UserRole } from '../../types'; +import React, { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { + User, + Mail, + Lock, + CircleDollarSign, + Building2, + Shield, + AlertCircle, +} from "lucide-react"; +import { useAuth } from "../../context/AuthContext"; +import { Button } from "../../components/ui/Button"; +import { Input } from "../../components/ui/Input"; +import { UserRole } from "../../types"; export const RegisterPage: React.FC = () => { - const [name, setName] = useState(''); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [role, setRole] = useState('entrepreneur'); + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [role, setRole] = useState("entrepreneur"); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); - + const { register } = useAuth(); const navigate = useNavigate(); - + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); - + // Validate passwords match if (password !== confirmPassword) { - setError('Passwords do not match'); + setError("Passwords do not match"); return; } - + setIsLoading(true); - + try { - await register(name, email, password, role); - // Redirect based on user role - navigate(role === 'entrepreneur' ? '/dashboard/entrepreneur' : '/dashboard/investor'); + localStorage.setItem( + "userInfo", + JSON.stringify({ name, email, password, role }) + ); + console.log("hello"); + navigate("/fill-details"); + // await register(name, email, password, role); + // // Redirect based on role + // if (role === 'admin') { + // navigate('/dashboard/admin'); + // } else if (role === 'entrepreneur') { + // navigate('/dashboard/entrepreneur'); + // } else { + // navigate('/dashboard/investor'); + // } } catch (err) { setError((err as Error).message); setIsLoading(false); } }; - + return (
- - - - +

Create your account

- Join Business Nexus to connect with partners + Join TrustBridge AI to connect with partners

@@ -67,41 +84,57 @@ export const RegisterPage: React.FC = () => { {error}
)} - +
-
+
+ {/* Entrepreneur */} - + + {/* Investor */} + + {/* Admin */} +
- + { fullWidth startAdornment={} /> - + { fullWidth startAdornment={} /> - + { fullWidth startAdornment={} /> - + { fullWidth startAdornment={} /> - +
{ required className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded" /> -
- - - +
@@ -180,11 +218,14 @@ export const RegisterPage: React.FC = () => { Or
- +

- Already have an account?{' '} - + Already have an account?{" "} + Sign in

@@ -194,4 +235,4 @@ export const RegisterPage: React.FC = () => {
); -}; \ No newline at end of file +}; diff --git a/src/pages/chat/ChatPage.tsx b/src/pages/chat/ChatPage.tsx index 10c55b077..af11bdd9c 100644 --- a/src/pages/chat/ChatPage.tsx +++ b/src/pages/chat/ChatPage.tsx @@ -1,73 +1,164 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { useParams } from 'react-router-dom'; -import { Send, Phone, Video, Info, Smile } from 'lucide-react'; -import { Avatar } from '../../components/ui/Avatar'; -import { Button } from '../../components/ui/Button'; -import { Input } from '../../components/ui/Input'; -import { ChatMessage } from '../../components/chat/ChatMessage'; -import { ChatUserList } from '../../components/chat/ChatUserList'; -import { useAuth } from '../../context/AuthContext'; -import { Message } from '../../types'; -import { findUserById } from '../../data/users'; -import { getMessagesBetweenUsers, sendMessage, getConversationsForUser } from '../../data/messages'; -import { MessageCircle } from 'lucide-react'; +import React, { useState, useEffect, useRef } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { Send, Phone, Video, Info, Smile } from "lucide-react"; +import { Avatar } from "../../components/ui/Avatar"; +import { Button } from "../../components/ui/Button"; +import { Input } from "../../components/ui/Input"; +import { ChatMessage } from "../../components/chat/ChatMessage"; +import { ChatUserList } from "../../components/chat/ChatUserList"; +import { useAuth } from "../../context/AuthContext"; +import { Message, User } from "../../types"; +import { + getMessagesBetweenUsers, + getConversationsForUser, + saveMessagesBetweenUsers, + updateConversationsForUser, +} from "../../data/messages"; +import { MessageCircle } from "lucide-react"; +import { getUserFromDb } from "../../data/users"; +import { useSocket } from "../../context/SocketContext"; export const ChatPage: React.FC = () => { const { userId } = useParams<{ userId: string }>(); const { user: currentUser } = useAuth(); const [messages, setMessages] = useState([]); - const [newMessage, setNewMessage] = useState(''); - const [conversations, setConversations] = useState([]); + const [newMessage, setNewMessage] = useState(""); + const [isTyping, setIsTyping] = useState(false); + const [checkStatus, setCheckStatus] = useState(false); + const [conversation, setConversation] = useState(); const messagesEndRef = useRef(null); - - const chatPartner = userId ? findUserById(userId) : null; - + const [chatPartner, setChatPartner] = useState(null); + const [users, setUsers] = useState<[string, User][]>([]); + const { socket } = useSocket(); + + const navigate = useNavigate(); + + // Load conversations useEffect(() => { - // Load conversations - if (currentUser) { - setConversations(getConversationsForUser(currentUser.id)); - } - }, [currentUser]); - + const fetchConversation = async () => { + const conv = await getConversationsForUser(currentUser?.userId); + if (conv) setConversation(conv); + }; + fetchConversation(); + }, [userId, currentUser?.userId, messages]); + + // Fetch Partner Data, messages + useEffect(() => { + if (!currentUser?.userId || !userId) return; // guard clause + const fetchUserData = async () => { + try { + // Load partner Data + const partner = await getUserFromDb(userId); + setChatPartner(partner || null); + + // Load messages + const messages = await getMessagesBetweenUsers( + currentUser.userId, + userId + ); + setMessages(messages.length > 0 ? messages : []); + } catch (err) { + console.error("Error fetching user data:", err); + } + }; + + fetchUserData(); + }, [currentUser?.userId, userId]); + + // Set Last messages for each conversation useEffect(() => { - // Load messages between users - if (currentUser && userId) { - setMessages(getMessagesBetweenUsers(currentUser.id, userId)); + const fetchUsers = async () => { + const uniqueIds = Array.from(new Set(messages.map((m) => m.senderId))); + const usersData = await Promise.all( + uniqueIds.map(async (id) => { + const user = await getUserFromDb(id); + return [id, user] as [string, User]; + }) + ); + + setUsers(Object.fromEntries(usersData)); + }; + + if (messages.length > 0) { + fetchUsers(); } - }, [currentUser, userId]); - + }, [messages]); + + // Connect socket.io client + useEffect(() => { + // when user receive message + socket?.on("received-message", (message) => { + setMessages((prev) => [...prev, message]); + }); + + // when user get typing + socket?.on("is-typing", () => { + setIsTyping(true); + setTimeout(() => { + setIsTyping(false); + }, 2000); + }); + + // when user offline or status changed + socket?.on("check-user-status", () => { + setCheckStatus(!checkStatus); + }); + return () => { + socket?.off("send-messsage"); + socket?.off("received-messsage"); + socket?.off("accept-call"); + socket?.off("reject-call"); + }; + }, [currentUser?.userId]); + useEffect(() => { // Scroll to bottom of messages - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); - - const handleSendMessage = (e: React.FormEvent) => { + + // Hanlde send message + const handleSendMessage = async (e: React.FormEvent) => { e.preventDefault(); - + if (!newMessage.trim() || !currentUser || !userId) return; - - const message = sendMessage({ - senderId: currentUser.id, + + const message = { + senderId: currentUser.userId, receiverId: userId, - content: newMessage - }); - - setMessages([...messages, message]); - setNewMessage(''); - - // Update conversations - setConversations(getConversationsForUser(currentUser.id)); + content: newMessage, + isRead: false, + }; + + const msg = await saveMessagesBetweenUsers(message); + socket?.emit("send-message", msg); + setMessages((prev) => [...prev, msg]); + setNewMessage(""); + + // Update conversation + try { + const con = { + sender: currentUser?.userId, + receiver: userId, + lastMessage: { ...msg }, + }; + const updatedConv = await updateConversationsForUser(con); + if (updatedConv) { + setConversation(updatedConv); + } + } catch (err) { + console.error("Failed to update conversation", err); + } }; - + if (!currentUser) return null; - + return (
{/* Conversations sidebar */}
- +
- + {/* Main chat area */}
{/* Chat header */} @@ -79,37 +170,67 @@ export const ChatPage: React.FC = () => { src={chatPartner.avatarUrl} alt={chatPartner.name} size="md" - status={chatPartner.isOnline ? 'online' : 'offline'} + status={chatPartner.isOnline ? "online" : "offline"} className="mr-3" /> - +
-

{chatPartner.name}

-

- {chatPartner.isOnline ? 'Online' : 'Last seen recently'} +

+ {chatPartner.name} +

+

+ {isTyping + ? "is typing" + : chatPartner.isOnline + ? "online" + : "Last seen recently"}

- +
- + - +
- + {/* Messages container */}
{messages.length > 0 ? (
- {messages.map(message => ( + {messages.map((msg) => ( ))}
@@ -139,12 +261,16 @@ export const ChatPage: React.FC = () => {
-

No messages yet

-

Send a message to start the conversation

+

+ No messages yet +

+

+ Send a message to start the conversation +

)}
- + {/* Message input */}
@@ -157,16 +283,20 @@ export const ChatPage: React.FC = () => { > - + setNewMessage(e.target.value)} + onChange={(e) => { + e.preventDefault(); + socket?.emit("typing", chatPartner._id); + setNewMessage(e.target.value); + }} fullWidth className="flex-1" /> - +
); -}; \ No newline at end of file +}; diff --git a/src/pages/dashboard/AdminDashboard.tsx b/src/pages/dashboard/AdminDashboard.tsx new file mode 100644 index 000000000..2b4aab991 --- /dev/null +++ b/src/pages/dashboard/AdminDashboard.tsx @@ -0,0 +1,294 @@ +import React, { useEffect, useState } from "react"; +import { + Users, + TrendingUp, + AlertTriangle, + Shield, + MessageSquare, +} from "lucide-react"; +import { Card, CardBody, CardHeader } from "../../components/ui/Card"; +import { Badge } from "../../components/ui/Badge"; +import { Button } from "../../components/ui/Button"; +import { useAuth } from "../../context/AuthContext"; +import { Link } from "react-router-dom"; +import axios from "axios"; +import { FraudAndRiskDetectionChart } from "../../components/admin/FraudAndRiskDetectionChart"; +import { StartupGrowthChart } from "../../components/admin/StartupGrowthChart"; +import { StartupIndustryChart } from "../../components/admin/StartupIndustryChart"; +import { FundingChart } from "../../components/admin/FundingChart"; + +type ChartData = { + month: string; + inv?: number; + ent?: number; +}; + +export const AdminDashboard: React.FC = () => { + const { user } = useAuth(); + const [stats, setStats] = useState({ + startups: 0, + investors: 0, + supporters: 10, + campaigns: 0, + flagged: 0, + }); + + useEffect(() => { + const fetchData = async () => { + try { + const res = await axios.get(`${URL}/admin/dashboard`); + setStats(res.data); + } catch (error) { + console.error("Error fetching admin stats:", error); + } + }; + + fetchData(); + }, []); + + const [startupGrowthChartData, setStartupGrowthChartData] = useState< + ChartData[] + >([]); + const [industryGrowthChartData, setIndustryGrowthChartData] = useState([]); + const [fraudGrowthChartData, setFraudGrowthChartData] = useState([]); + + const fetchStartupGrowthChartData = async () => { + const res = await fetch( + "http://localhost:5000/admin/users/users-last-year" + ); + const data = await res.json(); + setStartupGrowthChartData(data); + }; + // const fetchFundingChartData = async () => { + // const res = await fetch( + // "http://localhost:5000/admin/users/users-last-year" + // ); + // const data = await res.json(); + // setStartupGrowthChartData(data); + // }; + + const fetchIndustryGrowthChartData = async () => { + const res = await fetch( + "http://localhost:5000/admin/users/startup-by-industry" + ); + const data = await res.json(); + setIndustryGrowthChartData(data); + }; + + const fetchFraudGrowthChartData = async () => { + const res = await fetch("http://localhost:5000/admin/risk-detection-flags"); + const data = await res.json(); + const { finalData } = data; + setFraudGrowthChartData(finalData); + }; + + useEffect(() => { + fetchStartupGrowthChartData(); + fetchIndustryGrowthChartData(); + fetchFraudGrowthChartData(); + }, []); + + if (!user) return null; + + return ( +
+
+
+

+ Welcome, {user.name} +

+

+ Admin overview — manage users, campaigns, and analytics +

+
+
+ + {/* Top summary cards */} +
+ + +
+
+ +
+
+

Users

+

+ {stats.investors || 10} +

+
+
+
+
+ + + +
+
+ +
+
+

Supporters

+

+ {stats.supporters || 10} +

+
+
+
+
+ + + +
+
+ +
+
+

Campaigns

+

+ {stats.campaigns || 10} +

+
+
+
+
+ + + +
+
+ +
+
+

Flagged

+

+ {stats.flagged || 10} +

+
+
+
+
+
+ + + +

+ Analytics & Reports +

+ + Manage + +
+ +
+
+ +

+ User Growth Chart +

+
+ +
+ +

+ Startup industry growthRate +

+
+
+
+ + {/* Management sections */} +
+ {/* Campaign Oversight */} + + +

+ Campaign Oversight +

+ Crowdfunding +
+ + +
+

+ Edit, approve, or remove user accounts. Monitor activity and + handle reports of fraudulent behavior.The user catched by + suspicious activity could be responsible for its own acts. the + admin will always viewing your activities either you are + approaching well and structured way or not .This info is just + filling the blanks. +

+
+ + +
+
+
+ +

+ Fund GrowthRate +

+
+
+
+ + {/* Fraud Detection */} + + +

+ Fraud & Risk Detection +

+ Security +
+ +
+

+ Edit, approve, or remove user accounts. Monitor activity and + handle reports of fraudulent behavior.The user catched by + suspicious activity could be responsible for its own acts. the + admin will always viewing your activities either you are + approaching well and structured way or not .This info is just + filling the blanks. +

+
+ + +
+
+
+ + +

+ Fraud and Risk Detection GrowthRate +

+
+
+
+ + {/* AI Assistant Moderation */} + + +

+ AI Assistant Management +

+ AI +
+ +

+ Moderate AI-generated campaign suggestions and approve automated + responses before they are shown to users. +

+
+ +
+
+
+
+
+ ); +}; diff --git a/src/pages/dashboard/EntrepreneurDashboard.tsx b/src/pages/dashboard/EntrepreneurDashboard.tsx index 6c1dd3b17..214fd6d6d 100644 --- a/src/pages/dashboard/EntrepreneurDashboard.tsx +++ b/src/pages/dashboard/EntrepreneurDashboard.tsx @@ -1,58 +1,87 @@ -import React, { useState, useEffect } from 'react'; -import { Link } from 'react-router-dom'; -import { Users, Bell, Calendar, TrendingUp, AlertCircle, PlusCircle } from 'lucide-react'; -import { Button } from '../../components/ui/Button'; -import { Card, CardBody, CardHeader } from '../../components/ui/Card'; -import { Badge } from '../../components/ui/Badge'; -import { CollaborationRequestCard } from '../../components/collaboration/CollaborationRequestCard'; -import { InvestorCard } from '../../components/investor/InvestorCard'; -import { useAuth } from '../../context/AuthContext'; -import { CollaborationRequest } from '../../types'; -import { getRequestsForEntrepreneur } from '../../data/collaborationRequests'; -import { investors } from '../../data/users'; +import React, { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; +import { + Users, + Bell, + Calendar, + TrendingUp, + AlertCircle, + PlusCircle, +} from "lucide-react"; +import { Button } from "../../components/ui/Button"; +import { Card, CardBody, CardHeader } from "../../components/ui/Card"; +import { Badge } from "../../components/ui/Badge"; +import { CollaborationRequestCard } from "../../components/collaboration/CollaborationRequestCard"; +import { InvestorCard } from "../../components/investor/InvestorCard"; +import { useAuth } from "../../context/AuthContext"; +import { CollaborationRequest } from "../../types"; +import { getRequestsForEntrepreneur, updateRequestStatus } from "../../data/collaborationRequests"; +import { getInvestorsFromDb } from "../../data/users"; export const EntrepreneurDashboard: React.FC = () => { const { user } = useAuth(); - const [collaborationRequests, setCollaborationRequests] = useState([]); - const [recommendedInvestors, setRecommendedInvestors] = useState(investors.slice(0, 3)); + console.log(user) + const [collaborationRequests, setCollaborationRequests] = useState< + CollaborationRequest[] + >([]); + const [recommendedInvestors, setRecommendedInvestors] = useState([]); useEffect(() => { - if (user) { - // Load collaboration requests - const requests = getRequestsForEntrepreneur(user.id); - setCollaborationRequests(requests); - } - }, [user]); - - const handleRequestStatusUpdate = (requestId: string, status: 'accepted' | 'rejected') => { - setCollaborationRequests(prevRequests => - prevRequests.map(req => - req.id === requestId ? { ...req, status } : req + const fetchData = async () => { + if (user) { + const investors = await getInvestorsFromDb(); + setRecommendedInvestors(investors); + } + }; + fetchData(); + }, []); + + if (!user) return null; + + useEffect(() => { + const fetchRequests = async () => { + const requests = await getRequestsForEntrepreneur(user?.userId); + setCollaborationRequests(Array.isArray(requests) ? requests : []); + }; + fetchRequests(); + }, [user?.userId]); + + const pendingRequests = + collaborationRequests.length > 0 && + Array.isArray(collaborationRequests) && + collaborationRequests.length > 0 + ? collaborationRequests.filter((req) => req.requestStatus === "pending") + : []; + + const handleRequestStatusUpdate = ( + requestId: string, + status: "accepted" | "rejected" + ) => { + setCollaborationRequests((prevRequests) => + prevRequests.map((req) => + req._id === requestId ? { ...req, requestStatus:status } : req ) ); + updateRequestStatus(requestId,status) }; - - if (!user) return null; - - const pendingRequests = collaborationRequests.filter(req => req.status === 'pending'); - + return (
-

Welcome, {user.name}

-

Here's what's happening with your startup today

+

+ Welcome, {user.name} +

+

+ Here's what's happening with your startup today +

- + - +
- + {/* Summary cards */}
@@ -62,13 +91,17 @@ export const EntrepreneurDashboard: React.FC = () => {
-

Pending Requests

-

{pendingRequests.length}

+

+ Pending Requests +

+

+ {pendingRequests.length} +

- +
@@ -76,15 +109,21 @@ export const EntrepreneurDashboard: React.FC = () => {
-

Total Connections

+

+ Total Connections +

- {collaborationRequests.filter(req => req.status === 'accepted').length} + { + collaborationRequests.filter( + (req) => req.requestStatus === "accepted" + ).length + }

- +
@@ -92,13 +131,15 @@ export const EntrepreneurDashboard: React.FC = () => {
-

Upcoming Meetings

+

+ Upcoming Meetings +

2

- +
@@ -106,29 +147,33 @@ export const EntrepreneurDashboard: React.FC = () => {
-

Profile Views

+

+ Profile Views +

24

- +
{/* Collaboration requests */}
-

Collaboration Requests

+

+ Collaboration Requests +

{pendingRequests.length} pending
- + {collaborationRequests.length > 0 ? (
- {collaborationRequests.map(request => ( + {collaborationRequests.map((request) => ( @@ -140,35 +185,45 @@ export const EntrepreneurDashboard: React.FC = () => {

No collaboration requests yet

-

When investors are interested in your startup, their requests will appear here

+

+ When investors are interested in your startup, their + requests will appear here +

)}
- + {/* Recommended investors */}
-

Recommended Investors

- +

+ Recommended Investors +

+ View all
- + - {recommendedInvestors.map(investor => ( - - ))} + {recommendedInvestors && recommendedInvestors.length > 0 ? ( + recommendedInvestors.map((investor, i) => ( +
+ +
+ )) + ) : ( +
No investors found
+ )}
); -}; \ No newline at end of file +}; diff --git a/src/pages/dashboard/InvestorDashboard.tsx b/src/pages/dashboard/InvestorDashboard.tsx index b72480abc..8348fe976 100644 --- a/src/pages/dashboard/InvestorDashboard.tsx +++ b/src/pages/dashboard/InvestorDashboard.tsx @@ -1,72 +1,101 @@ -import React, { useState } from 'react'; -import { Link } from 'react-router-dom'; -import { Users, PieChart, Filter, Search, PlusCircle } from 'lucide-react'; -import { Button } from '../../components/ui/Button'; -import { Card, CardBody, CardHeader } from '../../components/ui/Card'; -import { Input } from '../../components/ui/Input'; -import { Badge } from '../../components/ui/Badge'; -import { EntrepreneurCard } from '../../components/entrepreneur/EntrepreneurCard'; -import { useAuth } from '../../context/AuthContext'; -import { Entrepreneur } from '../../types'; -import { entrepreneurs } from '../../data/users'; -import { getRequestsFromInvestor } from '../../data/collaborationRequests'; +import React, { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { Users, PieChart, Filter, Search, PlusCircle } from "lucide-react"; +import { Button } from "../../components/ui/Button"; +import { Card, CardBody, CardHeader } from "../../components/ui/Card"; +import { Input } from "../../components/ui/Input"; +import { Badge } from "../../components/ui/Badge"; +import { EntrepreneurCard } from "../../components/entrepreneur/EntrepreneurCard"; +import { useAuth } from "../../context/AuthContext"; +import { getRequestsFromInvestor } from "../../data/collaborationRequests"; +import { getEnterprenuerFromDb } from "../../data/users"; +import { CollaborationRequest, Entrepreneur } from "../../types"; export const InvestorDashboard: React.FC = () => { const { user } = useAuth(); - const [searchQuery, setSearchQuery] = useState(''); + const [searchQuery, setSearchQuery] = useState(""); const [selectedIndustries, setSelectedIndustries] = useState([]); - if (!user) return null; - + // Get collaboration requests sent by this investor - const sentRequests = getRequestsFromInvestor(user.id); - const requestedEntrepreneurIds = sentRequests.map(req => req.entrepreneurId); - + + // const requestedEntrepreneurIds = sentRequests.map(req => req.entrepreneurId); + const [entrepreneurs, setEnterprenuers] = useState([]); + const [sentRequests, setSentRequests] = useState([]); + + useEffect(() => { + const fetchData = async () => { + if (user) { + const entrepreneurs = await getEnterprenuerFromDb(); + setEnterprenuers(entrepreneurs); + entrepreneurs.map((e) => { + industries.push(e.industry); + }); + } + }; + fetchData(); + }, []); + + useEffect(() => { + const fetchData = async () => { + const requests = await getRequestsFromInvestor(user.userId); + setSentRequests(requests); + }; + fetchData(); + }, [user.userId]); + // Filter entrepreneurs based on search and industry filters - const filteredEntrepreneurs = entrepreneurs.filter(entrepreneur => { + const filteredEntrepreneurs = entrepreneurs.filter((entrepreneur) => { // Search filter - const matchesSearch = searchQuery === '' || + const matchesSearch = + searchQuery === "" || entrepreneur.name.toLowerCase().includes(searchQuery.toLowerCase()) || - entrepreneur.startupName.toLowerCase().includes(searchQuery.toLowerCase()) || - entrepreneur.industry.toLowerCase().includes(searchQuery.toLowerCase()) || - entrepreneur.pitchSummary.toLowerCase().includes(searchQuery.toLowerCase()); - + entrepreneur.startupName + .toLowerCase() + .includes(searchQuery.toLowerCase()) || + entrepreneur?.industry.toLowerCase().includes(searchQuery.toLowerCase()) || + entrepreneur?.pitchSummary + .toLowerCase() + .includes(searchQuery.toLowerCase()); + // Industry filter - const matchesIndustry = selectedIndustries.length === 0 || + const matchesIndustry = + selectedIndustries.length === 0 || selectedIndustries.includes(entrepreneur.industry); - + return matchesSearch && matchesIndustry; }); - + + const industries = entrepreneurs && entrepreneurs.map((enter) => enter.industry); + // Get unique industries for filter - const industries = Array.from(new Set(entrepreneurs.map(e => e.industry))); - + // Toggle industry selection const toggleIndustry = (industry: string) => { - setSelectedIndustries(prevSelected => + setSelectedIndustries((prevSelected) => prevSelected.includes(industry) - ? prevSelected.filter(i => i !== industry) + ? prevSelected.filter((i) => i !== industry) : [...prevSelected, industry] ); }; - + return (
-

Discover Startups

-

Find and connect with promising entrepreneurs

+

+ Discover Startups +

+

+ Find and connect with promising entrepreneurs +

- + - +
- + {/* Filters and search */}
@@ -78,17 +107,21 @@ export const InvestorDashboard: React.FC = () => { startAdornment={} />
- +
- Filter by: - + + Filter by: + +
- {industries.map(industry => ( + {industries.map((industry) => ( toggleIndustry(industry)} > @@ -99,7 +132,7 @@ export const InvestorDashboard: React.FC = () => {
- + {/* Stats summary */}
@@ -109,13 +142,17 @@ export const InvestorDashboard: React.FC = () => {
-

Total Startups

-

{entrepreneurs.length}

+

+ Total Startups +

+

+ {entrepreneurs && entrepreneurs.length} +

- +
@@ -123,13 +160,17 @@ export const InvestorDashboard: React.FC = () => {
-

Industries

-

{industries.length}

+

+ Industries +

+

+ {industries.length} +

- +
@@ -137,41 +178,48 @@ export const InvestorDashboard: React.FC = () => {
-

Your Connections

+

+ Your Connections +

- {sentRequests.filter(req => req.status === 'accepted').length} + {sentRequests && + sentRequests.filter( + (req) => req.requestStatus === "accepted" + ).length}

- + {/* Entrepreneurs grid */}
-

Featured Startups

+

+ Featured Startups +

- + - {filteredEntrepreneurs.length > 0 ? ( + {entrepreneurs && entrepreneurs.length > 0 ? (
- {filteredEntrepreneurs.map(entrepreneur => ( - - ))} + {entrepreneurs && + entrepreneurs.map((entrepreneur) => ( +
+ +
+ ))}
) : (

No startups match your filters

-
); -}; \ No newline at end of file +}; diff --git a/src/pages/deals/DealsPage.tsx b/src/pages/deals/DealsPage.tsx index ea26870c5..85c1f5dcc 100644 --- a/src/pages/deals/DealsPage.tsx +++ b/src/pages/deals/DealsPage.tsx @@ -1,156 +1,186 @@ -import React, { useState } from 'react'; -import { Search, Filter, DollarSign, TrendingUp, Users, Calendar } from 'lucide-react'; -import { Card, CardHeader, CardBody } from '../../components/ui/Card'; -import { Input } from '../../components/ui/Input'; -import { Button } from '../../components/ui/Button'; -import { Badge } from '../../components/ui/Badge'; -import { Avatar } from '../../components/ui/Avatar'; +import React, { useState } from "react"; +import { + Search, + Filter, + DollarSign, + TrendingUp, + Users, + Calendar, +} from "lucide-react"; +import { Card, CardHeader, CardBody } from "../../components/ui/Card"; +import { Input } from "../../components/ui/Input"; +import { Button } from "../../components/ui/Button"; +import { Badge } from "../../components/ui/Badge"; +import { Avatar } from "../../components/ui/Avatar"; -const deals = [ - { - id: 1, - startup: { - name: 'TechWave AI', - logo: 'https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg', - industry: 'FinTech' - }, - amount: '$1.5M', - equity: '15%', - status: 'Due Diligence', - stage: 'Series A', - lastActivity: '2024-02-15' - }, - { - id: 2, - startup: { - name: 'GreenLife Solutions', - logo: 'https://images.pexels.com/photos/614810/pexels-photo-614810.jpeg', - industry: 'CleanTech' - }, - amount: '$2M', - equity: '20%', - status: 'Term Sheet', - stage: 'Seed', - lastActivity: '2024-02-10' - }, - { - id: 3, - startup: { - name: 'HealthPulse', - logo: 'https://images.pexels.com/photos/415829/pexels-photo-415829.jpeg', - industry: 'HealthTech' - }, - amount: '$800K', - equity: '12%', - status: 'Negotiation', - stage: 'Pre-seed', - lastActivity: '2024-02-05' - } -]; +interface Deal { + id: number; + startup: { + name: string; + logo: string; + industry: string; + }; + amount: string; + equity: string; + status: string; + stage: string; + lastActivity: string; +} export const DealsPage: React.FC = () => { - const [searchQuery, setSearchQuery] = useState(''); + const [deals, setDeals] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); const [selectedStatus, setSelectedStatus] = useState([]); - - const statuses = ['Due Diligence', 'Term Sheet', 'Negotiation', 'Closed', 'Passed']; - + const [showForm, setShowForm] = useState(false); + const [newDeal, setNewDeal] = useState({ + name: "", + logo: "", + industry: "", + amount: "", + equity: "", + status: "Due Diligence", + stage: "Seed", + }); + + const statuses = ["Due Diligence", "Term Sheet", "Negotiation", "Closed", "Passed"]; + const toggleStatus = (status: string) => { - setSelectedStatus(prev => - prev.includes(status) - ? prev.filter(s => s !== status) - : [...prev, status] + setSelectedStatus((prev) => + prev.includes(status) ? prev.filter((s) => s !== status) : [...prev, status] ); }; - + const getStatusColor = (status: string) => { switch (status) { - case 'Due Diligence': - return 'primary'; - case 'Term Sheet': - return 'secondary'; - case 'Negotiation': - return 'accent'; - case 'Closed': - return 'success'; - case 'Passed': - return 'error'; + case "Due Diligence": + return "primary"; + case "Term Sheet": + return "secondary"; + case "Negotiation": + return "accent"; + case "Closed": + return "success"; + case "Passed": + return "error"; default: - return 'gray'; + return "gray"; } }; - + + const handleAddDeal = () => { + if (!newDeal.name || !newDeal.amount) return; + + const deal: Deal = { + id: Date.now(), + startup: { + name: newDeal.name, + logo: newDeal.logo || "https://via.placeholder.com/50", + industry: newDeal.industry || "Unknown", + }, + amount: newDeal.amount, + equity: newDeal.equity, + status: newDeal.status, + stage: newDeal.stage, + lastActivity: new Date().toISOString(), + }; + + setDeals((prev) => [...prev, deal]); + setNewDeal({ + name: "", + logo: "", + industry: "", + amount: "", + equity: "", + status: "Due Diligence", + stage: "Seed", + }); + setShowForm(false); + }; + + const filteredDeals = deals.filter( + (deal) => + (deal.startup.name.toLowerCase().includes(searchQuery.toLowerCase()) || + deal.startup.industry.toLowerCase().includes(searchQuery.toLowerCase())) && + (selectedStatus.length === 0 || selectedStatus.includes(deal.status)) + ); + return (

Investment Deals

-

Track and manage your investment pipeline

+

+ Track and manage your investment pipeline +

- -
- - {/* Stats */} -
- - -
-
- -
-
-

Total Investment

-

$4.3M

-
-
-
-
- - - -
-
- -
-
-

Active Deals

-

8

-
-
-
-
- - - -
-
- -
-
-

Portfolio Companies

-

12

-
-
-
-
- + + {/* Add Deal Form */} + {showForm && ( - -
-
- -
-
-

Closed This Month

-

2

-
-
+ +

New Deal

+
+ + setNewDeal({ ...newDeal, name: e.target.value })} + /> + setNewDeal({ ...newDeal, logo: e.target.value })} + /> + + setNewDeal({ ...newDeal, industry: e.target.value }) + } + /> + + setNewDeal({ ...newDeal, amount: e.target.value }) + } + /> + + setNewDeal({ ...newDeal, equity: e.target.value }) + } + /> + setNewDeal({ ...newDeal, stage: e.target.value })} + /> + + +
-
- + )} + {/* Filters */}
@@ -162,15 +192,19 @@ export const DealsPage: React.FC = () => { fullWidth />
- +
- {statuses.map(status => ( + {statuses.map((status) => ( toggleStatus(status)} > @@ -181,92 +215,96 @@ export const DealsPage: React.FC = () => {
- + {/* Deals table */}

Active Deals

-
- - - - - - - - - - - - - - {deals.map(deal => ( - - + + + + + + + + ))} + +
- Startup - - Amount - - Equity - - Status - - Stage - - Last Activity - - Actions -
-
- -
-
- {deal.startup.name} -
-
- {deal.startup.industry} + {filteredDeals.length === 0 ? ( +

No deals found.

+ ) : ( +
+ + + + + + + + + + + + + + {filteredDeals.map((deal) => ( + + - - - - - - - - ))} - -
+ Startup + + Amount + + Equity + + Status + + Stage + + Last Activity + + Actions +
+
+ +
+
+ {deal.startup.name} +
+
+ {deal.startup.industry} +
- -
-
{deal.amount}
-
-
{deal.equity}
-
- - {deal.status} - - -
{deal.stage}
-
-
- {new Date(deal.lastActivity).toLocaleDateString()} -
-
- -
-
+
+
{deal.amount}
+
+
{deal.equity}
+
+ + {deal.status} + + +
{deal.stage}
+
+
+ {new Date(deal.lastActivity).toLocaleDateString()} +
+
+ +
+
+ )}
); -}; \ No newline at end of file +}; diff --git a/src/pages/documents/DocumentsPage.tsx b/src/pages/documents/DocumentsPage.tsx index 1e325d3c9..f3e5ffa2d 100644 --- a/src/pages/documents/DocumentsPage.tsx +++ b/src/pages/documents/DocumentsPage.tsx @@ -1,45 +1,37 @@ -import React from 'react'; -import { FileText, Upload, Download, Trash2, Share2 } from 'lucide-react'; -import { Card, CardHeader, CardBody } from '../../components/ui/Card'; -import { Button } from '../../components/ui/Button'; -import { Badge } from '../../components/ui/Badge'; +import React, { useState } from "react"; +import { FileText, Upload, Download, Trash2, Share2 } from "lucide-react"; +import { Card, CardHeader, CardBody } from "../../components/ui/Card"; +import { Button } from "../../components/ui/Button"; +import { Badge } from "../../components/ui/Badge"; -const documents = [ - { - id: 1, - name: 'Pitch Deck 2024.pdf', - type: 'PDF', - size: '2.4 MB', - lastModified: '2024-02-15', - shared: true - }, - { - id: 2, - name: 'Financial Projections.xlsx', - type: 'Spreadsheet', - size: '1.8 MB', - lastModified: '2024-02-10', - shared: false - }, - { - id: 3, - name: 'Business Plan.docx', - type: 'Document', - size: '3.2 MB', - lastModified: '2024-02-05', - shared: true - }, - { - id: 4, - name: 'Market Research.pdf', - type: 'PDF', - size: '5.1 MB', - lastModified: '2024-01-28', - shared: false - } -]; +interface Document { + id: number; + name: string; + type: string; + size: string; + lastModified: string; + shared: boolean; +} export const DocumentsPage: React.FC = () => { + const [documents, setDocuments] = useState([]); + + const handleUpload = (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files) return; + + const uploadedDocs: Document[] = Array.from(files).map((file, idx) => ({ + id: documents.length + idx + 1, + name: file.name, + type: file.type || "Unknown", + size: (file.size / (1024 * 1024)).toFixed(2) + " MB", + lastModified: new Date(file.lastModified).toISOString().split("T")[0], + shared: false, + })); + + setDocuments((prev) => [...prev, ...uploadedDocs]); + }; + return (
@@ -47,12 +39,20 @@ export const DocumentsPage: React.FC = () => {

Documents

Manage your startup's important files

- - + +
- +
{/* Storage info */} @@ -66,39 +66,26 @@ export const DocumentsPage: React.FC = () => { 12.5 GB
-
+
Available 7.5 GB
- -
-

Quick Access

-
- - - - -
-
- + {/* Document list */}
-

All Documents

+

+ All Documents +

- + - +
); -}; \ No newline at end of file +}; diff --git a/src/pages/entrepreneurs/EntrepreneursPage.tsx b/src/pages/entrepreneurs/EntrepreneursPage.tsx index bbcf8bf31..530478b6c 100644 --- a/src/pages/entrepreneurs/EntrepreneursPage.tsx +++ b/src/pages/entrepreneurs/EntrepreneursPage.tsx @@ -1,17 +1,29 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Search, Filter, MapPin } from 'lucide-react'; import { Input } from '../../components/ui/Input'; import { Card, CardHeader, CardBody } from '../../components/ui/Card'; -import { Badge } from '../../components/ui/Badge'; import { EntrepreneurCard } from '../../components/entrepreneur/EntrepreneurCard'; -import { entrepreneurs } from '../../data/users'; +import { useAuth } from '../../context/AuthContext'; +import { getEnterprenuerFromDb } from '../../data/users'; +import { Entrepreneur } from '../../types'; export const EntrepreneursPage: React.FC = () => { const [searchQuery, setSearchQuery] = useState(''); const [selectedIndustries, setSelectedIndustries] = useState([]); const [selectedFundingRange, setSelectedFundingRange] = useState([]); - + const {user} =useAuth(); // Get unique industries and funding ranges + const [entrepreneurs,setEnterprenuers] = useState([]); + + useEffect(() => { + const fetchData = async()=>{ + if (user) { + const entrepreneurs = await getEnterprenuerFromDb(); + setEnterprenuers(entrepreneurs); + } + } + fetchData(); + }, []); const allIndustries = Array.from(new Set(entrepreneurs.map(e => e.industry))); const fundingRanges = ['< $500K', '$500K - $1M', '$1M - $5M', '> $5M']; @@ -154,7 +166,7 @@ export const EntrepreneursPage: React.FC = () => {
{filteredEntrepreneurs.map(entrepreneur => ( ))} diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx new file mode 100644 index 000000000..214136086 --- /dev/null +++ b/src/pages/home/HomePage.tsx @@ -0,0 +1,198 @@ +import React from "react"; +import { Navbar } from "../../components/home/Navbar"; + +interface CompaignProps { + image: string; + title: string; +} +interface FundraiserProps { + image: string; + fundNeeded: string; + company: string; + description: string; +} +interface SuccessfulCompanyProps { + image: string; + company: string; + description: string; + exits: number; +} +export const HomePage: React.FC = () => { + const CompaignDiv: React.FC = ({ image, title }) => { + return ( +
+ pic +
+
+

+ {title} +

+
+
+ ); + }; + const FundeRaiserDiv: React.FC = ({ + image, + fundNeeded, + company, + description, + }) => { + return ( +
+ pic +
+
+

{company}

+

{description}

+
+ + Fund Needed: $ + +

{fundNeeded}

+
+
+
+
+ ); + }; + const SuccessfulCompanyDiv: React.FC = ({ + image, + company, + description, + exits, + }) => { + return ( +
+
+ pic + {/*
*/} +
+

+ Company: + {company} +

+

+ Description: + {description} +

+
+ Total exits: +

{exits}

+
+
+
+
+ ); + }; + return ( +
+ +
+ +
+
+
+
+

+ Recent Compaigns.. +

+
+ + + +
+
+ +
+
+

+ The plateform where Entrepreneur and investors can + collaborate,make deals and can make secure & successful + transactions.{" "} +

+
+ + +
+
+
+ +
+

+ Fundraisers.. +

+
+ + + +
+
+
+
+

+ Successful Entrepreneurs.. +

+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ ); +}; diff --git a/src/pages/investors/InvestorsPage.tsx b/src/pages/investors/InvestorsPage.tsx index 0f234cf4d..345230158 100644 --- a/src/pages/investors/InvestorsPage.tsx +++ b/src/pages/investors/InvestorsPage.tsx @@ -1,61 +1,86 @@ -import React, { useState } from 'react'; -import { Search, Filter, MapPin } from 'lucide-react'; -import { Input } from '../../components/ui/Input'; -import { Card, CardHeader, CardBody } from '../../components/ui/Card'; -import { Badge } from '../../components/ui/Badge'; -import { InvestorCard } from '../../components/investor/InvestorCard'; -import { investors } from '../../data/users'; +import React, { useEffect, useState } from "react"; +import { Search, Filter, MapPin } from "lucide-react"; +import { Input } from "../../components/ui/Input"; +import { Card, CardHeader, CardBody } from "../../components/ui/Card"; +import { Badge } from "../../components/ui/Badge"; +import { InvestorCard } from "../../components/investor/InvestorCard"; +import { useAuth } from "../../context/AuthContext"; +import { getInvestorsFromDb } from "../../data/users"; + +import { Investor } from "../../types"; export const InvestorsPage: React.FC = () => { - const [searchQuery, setSearchQuery] = useState(''); + const [searchQuery, setSearchQuery] = useState(""); const [selectedStages, setSelectedStages] = useState([]); const [selectedInterests, setSelectedInterests] = useState([]); - + + const [investors, setInvestors] = useState([]); + const { user } = useAuth(); + + useEffect(() => { + const fetchData = async () => { + if (user) { + const investors = await getInvestorsFromDb(); + setInvestors(investors ? investors : []); + } + }; + fetchData(); + }, []); + if (!user) return null; // Get unique investment stages and interests - const allStages = Array.from(new Set(investors.flatMap(i => i.investmentStage))); - const allInterests = Array.from(new Set(investors.flatMap(i => i.investmentInterests))); - + const allStages = Array.from( + new Set(investors.flatMap((i) => i.investmentStage || "")) + ); + const allInterests = Array.from( + new Set(investors.flatMap((i) => i.investmentInterests || "")) + ); + // Filter investors based on search and filters - const filteredInvestors = investors.filter(investor => { - const matchesSearch = searchQuery === '' || - investor.name.toLowerCase().includes(searchQuery.toLowerCase()) || - investor.bio.toLowerCase().includes(searchQuery.toLowerCase()) || - investor.investmentInterests.some(interest => - interest.toLowerCase().includes(searchQuery.toLowerCase()) + const filteredInvestors = investors && investors.filter((investor) => { + const matchesSearch = + searchQuery === "" || + investor.name?.toLowerCase().includes(searchQuery.toLowerCase()) || + investor.bio?.toLowerCase().includes(searchQuery.toLowerCase()) || + investor.investmentInterests?.some((interest) => + interest?.toLowerCase().includes(searchQuery.toLowerCase()) ); - - const matchesStages = selectedStages.length === 0 || - investor.investmentStage.some(stage => selectedStages.includes(stage)); - - const matchesInterests = selectedInterests.length === 0 || - investor.investmentInterests.some(interest => selectedInterests.includes(interest)); - + + const matchesStages = + selectedStages.length === 0 || + investor.investmentStage?.some((stage) => selectedStages.includes(stage)); + + const matchesInterests = + selectedInterests.length === 0 || + investor.investmentInterests?.some((interest) => + selectedInterests.includes(interest) + ); + return matchesSearch && matchesStages && matchesInterests; }); - + const toggleStage = (stage: string) => { - setSelectedStages(prev => - prev.includes(stage) - ? prev.filter(s => s !== stage) - : [...prev, stage] + setSelectedStages((prev) => + prev.includes(stage) ? prev.filter((s) => s !== stage) : [...prev, stage] ); }; - + const toggleInterest = (interest: string) => { - setSelectedInterests(prev => + setSelectedInterests((prev) => prev.includes(interest) - ? prev.filter(i => i !== interest) + ? prev.filter((i) => i !== interest) : [...prev, interest] ); }; - + return (

Find Investors

-

Connect with investors who match your startup's needs

+

+ Connect with investors who match your startup's needs +

- +
{/* Filters sidebar */}
@@ -65,16 +90,18 @@ export const InvestorsPage: React.FC = () => {
-

Investment Stage

+

+ Investment Stage +

- {allStages.map(stage => ( + {allStages.map((stage) => (
- +
-

Investment Interests

+

+ Investment Interests +

- {allInterests.map(interest => ( + {allInterests.map((interest) => ( toggleInterest(interest)} > @@ -98,9 +131,11 @@ export const InvestorsPage: React.FC = () => { ))}
- +
-

Location

+

+ Location +

- + {/* Main content */}
@@ -130,7 +165,7 @@ export const InvestorsPage: React.FC = () => { startAdornment={} fullWidth /> - +
@@ -138,17 +173,14 @@ export const InvestorsPage: React.FC = () => {
- +
- {filteredInvestors.map(investor => ( - + {filteredInvestors.map((investor) => ( + ))}
); -}; \ No newline at end of file +}; diff --git a/src/pages/messages/MessagesPage.tsx b/src/pages/messages/MessagesPage.tsx index c7589fb80..b6d9c31e8 100644 --- a/src/pages/messages/MessagesPage.tsx +++ b/src/pages/messages/MessagesPage.tsx @@ -1,33 +1,45 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useAuth } from '../../context/AuthContext'; -import { getConversationsForUser } from '../../data/messages'; -import { ChatUserList } from '../../components/chat/ChatUserList'; -// import { MessageCircle } from 'lucide-react'; +import React, { useEffect, useState } from "react"; +import { useAuth } from "../../context/AuthContext"; +import { getConversationsForUser } from "../../data/messages"; +import { ChatUserList } from "../../components/chat/ChatUserList"; +import { MessageCircle } from "lucide-react"; +import { ChatConversation } from "../../types"; export const MessagesPage: React.FC = () => { const { user } = useAuth(); - const navigate = useNavigate(); - + const [conversation, setConversation] = useState(); + if (!user) return null; - - const conversations = getConversationsForUser(user.id); - + + useEffect(() => { + const fetchConversation = async () => { + const conv = await getConversationsForUser(user?.userId); + if (conv) { + setConversation(conv); + } + }; + + fetchConversation(); + }, [user?.userId]); + return ( -
- {conversations.length > 0 ? ( - +
+ {conversation? ( +
+ +
) : (
- {/* */} +

No messages yet

- Start connecting with entrepreneurs and investors to begin conversations + Start connecting with entrepreneurs and investors to begin + conversation

)}
); -}; \ No newline at end of file +}; diff --git a/src/pages/notifications/NotificationsPage.tsx b/src/pages/notifications/NotificationsPage.tsx index 3bb806c25..5872c07eb 100644 --- a/src/pages/notifications/NotificationsPage.tsx +++ b/src/pages/notifications/NotificationsPage.tsx @@ -1,112 +1,90 @@ -import React from 'react'; -import { Bell, MessageCircle, UserPlus, DollarSign } from 'lucide-react'; -import { Card, CardBody } from '../../components/ui/Card'; -import { Avatar } from '../../components/ui/Avatar'; -import { Badge } from '../../components/ui/Badge'; -import { Button } from '../../components/ui/Button'; - -const notifications = [ - { - id: 1, - type: 'message', - user: { - name: 'Sarah Johnson', - avatar: 'https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg' - }, - content: 'sent you a message about your startup', - time: '5 minutes ago', - unread: true - }, - { - id: 2, - type: 'connection', - user: { - name: 'Michael Rodriguez', - avatar: 'https://images.pexels.com/photos/2379004/pexels-photo-2379004.jpeg' - }, - content: 'accepted your connection request', - time: '2 hours ago', - unread: true - }, - { - id: 3, - type: 'investment', - user: { - name: 'Jennifer Lee', - avatar: 'https://images.pexels.com/photos/1181686/pexels-photo-1181686.jpeg' - }, - content: 'showed interest in investing in your startup', - time: '1 day ago', - unread: false - } -]; +import React from "react"; +import { Bell, MessageCircle, UserPlus, DollarSign } from "lucide-react"; +import { Card, CardBody } from "../../components/ui/Card"; +import { Avatar } from "../../components/ui/Avatar"; +import { Badge } from "../../components/ui/Badge"; +import { Button } from "../../components/ui/Button"; +import { useAuth } from "../../context/AuthContext"; export const NotificationsPage: React.FC = () => { + const { user } = useAuth(); + const notifications = user?.notifications || []; + const getNotificationIcon = (type: string) => { switch (type) { - case 'message': + case "message": return ; - case 'connection': + case "connection": return ; - case 'investment': + case "investment": return ; default: return ; } }; - + return (

Notifications

-

Stay updated with your network activity

+

+ Stay updated with your network activity +

- - + + {notifications.length > 0 && ( + + )}
- +
- {notifications.map(notification => ( - - - - -
-
- - {notification.user.name} - - {notification.unread && ( - New - )} -
- -

- {notification.content} -

- -
- {getNotificationIcon(notification.type)} - {notification.time} + {notifications.length > 0 ? ( + notifications.map((notification: any) => ( + + + + +
+
+ + {notification.user.name} + + {notification.unread && ( + + New + + )} +
+ +

{notification.content}

+ +
+ {getNotificationIcon(notification.type)} + {notification.time} +
-
- - - ))} + + + )) + ) : ( +

+ No notifications found 🚀 +

+ )}
); -}; \ No newline at end of file +}; diff --git a/src/pages/profile/EntrepreneurProfile.tsx b/src/pages/profile/EntrepreneurProfile.tsx index 3c05a18e6..146c333b6 100644 --- a/src/pages/profile/EntrepreneurProfile.tsx +++ b/src/pages/profile/EntrepreneurProfile.tsx @@ -1,56 +1,121 @@ -import React from 'react'; -import { useParams, Link } from 'react-router-dom'; -import { MessageCircle, Users, Calendar, Building2, MapPin, UserCircle, FileText, DollarSign, Send } from 'lucide-react'; -import { Avatar } from '../../components/ui/Avatar'; -import { Button } from '../../components/ui/Button'; -import { Card, CardBody, CardHeader } from '../../components/ui/Card'; -import { Badge } from '../../components/ui/Badge'; -import { useAuth } from '../../context/AuthContext'; -import { findUserById } from '../../data/users'; -import { createCollaborationRequest, getRequestsFromInvestor } from '../../data/collaborationRequests'; -import { Entrepreneur } from '../../types'; - -export const EntrepreneurProfile: React.FC = () => { +import React, { useEffect, useState } from "react"; +import { useParams, Link, useNavigate } from "react-router-dom"; +import { + MessageCircle, + Users, + Calendar, + Building2, + MapPin, + UserCircle, + FileText, + DollarSign, + Send, +} from "lucide-react"; +import { Avatar } from "../../components/ui/Avatar"; +import { Button } from "../../components/ui/Button"; +import { Card, CardBody, CardHeader } from "../../components/ui/Card"; +import { Badge } from "../../components/ui/Badge"; +import { useAuth } from "../../context/AuthContext"; +import { + checkRequestsFromInvestor, + createCollaborationRequest, +} from "../../data/collaborationRequests"; +import { Entrepreneur } from "../../types"; +import { AmountMeasureWithTags, getEnterpreneurById } from "../../data/users"; + +type Props = { + userId?: string | undefined; +}; +export const EntrepreneurProfile: React.FC = ({ userId }) => { const { id } = useParams<{ id: string }>(); const { user: currentUser } = useAuth(); - - // Fetch entrepreneur data - const entrepreneur = findUserById(id || '') as Entrepreneur | null; - - if (!entrepreneur || entrepreneur.role !== 'entrepreneur') { + const [entrepreneur, setEnterpreneur] = useState(); + const [hasRequestedCollaboration, setHasRequestedCollaboration] = + useState(); + const navigate = useNavigate(); + + const [valuation, setValuation] = useState(0); + + useEffect(() => { + const fetchEntrepreneur = async () => { + if (id) { + const entrepreneur = await getEnterpreneurById(id); + setEnterpreneur(entrepreneur); + } else { + const entrepreneur = await getEnterpreneurById(userId); + setEnterpreneur(entrepreneur); + } + }; + fetchEntrepreneur(); + }, [id, userId]); + + useEffect(() => { + const calculateValuation = () => { + if (entrepreneur?.growthRate && entrepreneur?.profitMargin) + return ( + 5 * + (1 + + entrepreneur?.growthRate / 100 + + entrepreneur?.profitMargin / 100) + ); + }; + const nichevalue = calculateValuation(); + // ensure we multiply two numbers: use a numeric default for nichevalue and revenue + const base = nichevalue ?? 1; + const revenue = entrepreneur?.revenue ?? 0; + setValuation(base * revenue); + }, [entrepreneur]); + + useEffect(() => { + const checkInvestor = async () => { + if (currentUser?.userId && id) { + const request = await checkRequestsFromInvestor(currentUser.userId, id); + console.log(request); + setHasRequestedCollaboration(Boolean(request)); + } + }; + checkInvestor(); + }, [currentUser?.userId, id]); + + if (!currentUser) return null; + if (!entrepreneur || entrepreneur.role !== "entrepreneur") { return (
-

Entrepreneur not found

-

The entrepreneur profile you're looking for doesn't exist or has been removed.

+

+ Entrepreneur not found +

+

+ The entrepreneur profile you're looking for doesn't exist or has been + removed. +

+ s - +
); } - - const isCurrentUser = currentUser?.id === entrepreneur.id; - const isInvestor = currentUser?.role === 'investor'; - + + const isCurrentUser = currentUser?.userId === entrepreneur?.userId; + const isInvestor = currentUser?.role === "investor"; + const isAdmin = currentUser?.role === "admin"; // Check if the current investor has already sent a request to this entrepreneur - const hasRequestedCollaboration = isInvestor && id - ? getRequestsFromInvestor(currentUser.id).some(req => req.entrepreneurId === id) - : false; - - const handleSendRequest = () => { + + const handleSendRequest = async () => { if (isInvestor && currentUser && id) { createCollaborationRequest( - currentUser.id, + currentUser.userId, id, `I'm interested in learning more about ${entrepreneur.startupName} and would like to explore potential investment opportunities.` ); - - // In a real app, we would refresh the data or update state - // For this demo, we'll force a page reload - window.location.reload(); + await setHasRequestedCollaboration(true); } }; - + + const fundAmount = valuation ?? 0; + return (
{/* Profile header */} @@ -61,63 +126,75 @@ export const EntrepreneurProfile: React.FC = () => { src={entrepreneur.avatarUrl} alt={entrepreneur.name} size="xl" - status={entrepreneur.isOnline ? 'online' : 'offline'} + status={entrepreneur.isOnline ? "online" : "offline"} className="mx-auto sm:mx-0" /> - +
-

{entrepreneur.name}

+

+ {entrepreneur.name} +

- Founder at {entrepreneur.startupName} + Founder at {entrepreneur.startupName || "--"}

- +
- {entrepreneur.industry} + {entrepreneur.industry || "--"} - {entrepreneur.location} + {entrepreneur.location || "--"} - Founded {entrepreneur.foundedYear} + Founded {entrepreneur.foundedYear || "--"} - {entrepreneur.teamSize} team members + {entrepreneur.teamSize || 0} team members
- +
- {!isCurrentUser && ( - <> - - - - - {isInvestor && ( - - )} - + {!isAdmin ? ( + !isCurrentUser && ( + <> + + + + + {isInvestor && ( + + )} + + ) + ) : ( + <> )} - + {isCurrentUser && ( @@ -125,7 +202,7 @@ export const EntrepreneurProfile: React.FC = () => {
- +
{/* Main content - left side */}
@@ -135,53 +212,67 @@ export const EntrepreneurProfile: React.FC = () => {

About

-

{entrepreneur.bio}

+

+ {entrepreneur.bio || "Say about yours..?"} +

- + {/* Startup Description */} -

Startup Overview

+

+ Startup Overview +

-

Problem Statement

+

+ Problem Statement +

- {entrepreneur?.pitchSummary?.split('.')[0]}. + {entrepreneur?.pitchSummary?.split(".")[0] || "--"}.

- +
-

Solution

+

+ Solution +

- {entrepreneur.pitchSummary} + {entrepreneur.pitchSummary || "--"}

- +
-

Market Opportunity

+

+ Market Opportunity +

- The {entrepreneur.industry} market is experiencing significant growth, with a projected CAGR of 14.5% through 2027. Our solution addresses key pain points in this expanding market. + {entrepreneur.marketOpportunity || "--"}

- +
-

Competitive Advantage

+

+ Competitive Advantage +

- Unlike our competitors, we offer a unique approach that combines innovative technology with deep industry expertise, resulting in superior outcomes for our customers. + {entrepreneur.advantage || "--"}

- + {/* Team */}

Team

- {entrepreneur.teamSize} members + + {entrepreneur.teamSize || 0} members +
@@ -193,11 +284,13 @@ export const EntrepreneurProfile: React.FC = () => { className="mr-3" />
-

{entrepreneur.name}

+

+ {entrepreneur.name} +

Founder & CEO

- +
{ className="mr-3" />
-

Alex Johnson

+

+ Alex Johnson +

CTO

- +
{ className="mr-3" />
-

Jessica Chen

+

+ Jessica Chen +

Head of Product

- - {entrepreneur.teamSize > 3 && ( + + {entrepreneur.teamSize && entrepreneur?.teamSize > 3 && (
-

+ {entrepreneur.teamSize - 3} more team members

+

+ + {entrepreneur?.teamSize - 3} more team members +

)}
- + {/* Sidebar - right side */}
{/* Funding Details */} @@ -244,44 +343,78 @@ export const EntrepreneurProfile: React.FC = () => {
- Current Round + Fund needed
-

{entrepreneur.fundingNeeded}

+

+ {AmountMeasureWithTags(entrepreneur.fundingNeeded ?? 0)} +

- +
Valuation -

$8M - $12M

+

+ ${AmountMeasureWithTags(valuation ?? 0)} +

- +
- Previous Funding -

$750K Seed (2022)

+ + Previous Funding + +

+ $750K Seed (2022) +

- +
- Funding Timeline + + Funding Timeline +
Pre-seed - Completed + 100000 + ? " text-green-800 bg-green-100" + : "text-yellow-800 bg-yellow-100" + } px-2 py-0.5 rounded-full`} + > + {fundAmount > 100000 ? "Completed" : "In progress"} +
Seed - Completed + 2500000 + ? " text-green-800 bg-green-100" + : "text-yellow-800 bg-yellow-100" + } px-2 py-0.5 rounded-full`} + > + {fundAmount > 2500000 ? "Completed" : "In progress"} +
Series A - In Progress + 20000000 + ? " text-green-800 bg-green-100" + : "text-yellow-800 bg-yellow-100" + } px-2 py-0.5 rounded-full`} + > + {fundAmount > 20000000 ? "Completed" : "In progress"} +
- + {/* Documents */} @@ -294,53 +427,62 @@ export const EntrepreneurProfile: React.FC = () => {
-

Pitch Deck

-

Updated 2 months ago

+

+ Pitch Deck +

+

+ Updated 2 months ago +

- +
- +
-

Business Plan

+

+ Business Plan +

Updated 1 month ago

- +
- +
-

Financial Projections

+

+ Financial Projections +

Updated 2 weeks ago

- +
- + {!isCurrentUser && isInvestor && (

- Request access to detailed documents and financials by sending a collaboration request. + Request access to detailed documents and financials by + sending a collaboration request.

- + {!hasRequestedCollaboration ? ( - ) : ( - )} @@ -352,4 +494,4 @@ export const EntrepreneurProfile: React.FC = () => {
); -}; \ No newline at end of file +}; diff --git a/src/pages/profile/InvestorProfile.tsx b/src/pages/profile/InvestorProfile.tsx index 22b722d62..8f8031245 100644 --- a/src/pages/profile/InvestorProfile.tsx +++ b/src/pages/profile/InvestorProfile.tsx @@ -1,35 +1,66 @@ -import React from 'react'; -import { useParams, Link } from 'react-router-dom'; -import { MessageCircle, Building2, MapPin, UserCircle, BarChart3, Briefcase } from 'lucide-react'; -import { Avatar } from '../../components/ui/Avatar'; -import { Button } from '../../components/ui/Button'; -import { Card, CardBody, CardHeader } from '../../components/ui/Card'; -import { Badge } from '../../components/ui/Badge'; -import { useAuth } from '../../context/AuthContext'; -import { findUserById } from '../../data/users'; -import { Investor } from '../../types'; +import React, { useEffect, useState } from "react"; +import { useParams, Link, useNavigate } from "react-router-dom"; +import { + MessageCircle, + Building2, + MapPin, + UserCircle, + BarChart3, + Briefcase, + DollarSign, +} from "lucide-react"; +import { Avatar } from "../../components/ui/Avatar"; +import { Button } from "../../components/ui/Button"; +import { Card, CardBody, CardHeader } from "../../components/ui/Card"; +import { Badge } from "../../components/ui/Badge"; +import { useAuth } from "../../context/AuthContext"; +import { getInvestorById } from "../../data/users"; +import { Investor } from "../../types"; -export const InvestorProfile: React.FC = () => { +type Props = { + userId?: string | undefined; +}; +export const InvestorProfile: React.FC = ({ userId }) => { const { id } = useParams<{ id: string }>(); const { user: currentUser } = useAuth(); - + const [investor, setInvestor] = useState(); + const navigate = useNavigate(); + // Fetch investor data - const investor = findUserById(id || '') as Investor | null; - - if (!investor || investor.role !== 'investor') { + useEffect(() => { + const fetchInvestors = async () => { + if (id) { + const investor = await getInvestorById(id); + setInvestor(investor); + } else { + const investor = await getInvestorById(userId); + setInvestor(investor); + } + }; + fetchInvestors(); + }, [id, userId]); + + if (!currentUser) return null; + if (!investor || investor.role !== "investor") { return (

Investor not found

-

The investor profile you're looking for doesn't exist or has been removed.

- - +

+ The investor profile you're looking for doesn't exist or has been + removed. +

+ +
); } - - const isCurrentUser = currentUser?.id === investor.id; - + + const isCurrentUser = currentUser?.userId === investor?.userId; + const isAdmin = currentUser?.role === "admin"; + return (
{/* Profile header */} @@ -40,44 +71,55 @@ export const InvestorProfile: React.FC = () => { src={investor.avatarUrl} alt={investor.name} size="xl" - status={investor.isOnline ? 'online' : 'offline'} + status={investor.isOnline ? "online" : "offline"} className="mx-auto sm:mx-0" /> - +
-

{investor.name}

+

+ {investor.name} +

- Investor • {investor.totalInvestments} investments + Investor • {investor.totalInvestments || 0} investments

- +
- San Francisco, CA + {investor?.location || "--"} - {investor.investmentStage.map((stage, index) => ( - {stage} - ))} + {investor.investmentStage && + investor.investmentStage.map((stage, index) => ( + + {stage} + + ))}
- +
- {!isCurrentUser && ( - - - + {!isAdmin ? ( + !isCurrentUser && ( + + + + ) + ) : ( + <> )} - + {isCurrentUser && ( @@ -85,7 +127,7 @@ export const InvestorProfile: React.FC = () => {
- +
{/* Main content - left side */}
@@ -95,169 +137,227 @@ export const InvestorProfile: React.FC = () => {

About

-

{investor.bio}

+

+ {investor.bio || "Say something about u..?"} +

- + {/* Investment Interests */} -

Investment Interests

+

+ Investment Interests +

-

Industries

+

+ Industries +

- {investor.investmentInterests.map((interest, index) => ( - {interest} - ))} + {(investor.investmentInterests && + investor.investmentInterests.map((interest, index) => ( + + {interest} + + ))) || + "Add some industries..?"}
- +
-

Investment Stages

+

+ Investment Stages +

- {investor.investmentStage.map((stage, index) => ( - {stage} - ))} + {(investor.investmentStage && + investor.investmentStage.map((stage, index) => ( + + {stage} + + ))) || + "Add investment stages..?"}
- +
-

Investment Criteria

+

+ Investment Criteria +

    -
  • - - Strong founding team with domain expertise -
  • -
  • - - Clear market opportunity and product-market fit -
  • -
  • - - Scalable business model with strong unit economics -
  • -
  • - - Potential for significant growth and market impact -
  • + {investor.investmentCriteria?.map((ic, idx) => ( +
  • + + {ic} +
  • + )) || "Set investment criteria..?"}
- + {/* Portfolio Companies */} -

Portfolio Companies

- {investor.portfolioCompanies.length} companies +

+ Portfolio Companies +

+ + {(investor.portfolioCompanies && + investor.portfolioCompanies.length) || + 0}{" "} + companies +
- {investor.portfolioCompanies.map((company, index) => ( -
-
- -
-
-

{company}

-

Invested in 2022

+ {(investor.portfolioCompanies && + investor.portfolioCompanies.map((company, index) => ( +
+
+ +
+
+

+ {company} +

+

+ Invested in 2022 +

+
-
- ))} + ))) || + "You don't invest in any company yet.."}
- + {/* Sidebar - right side */}
{/* Investment Details */} -

Investment Details

+

+ Investment Details +

- Investment Range -

- {investor.minimumInvestment} - {investor.maximumInvestment} + + Investment Range + +

+ + {(investor.minimumInvestment && + investor.minimumInvestment) || + 0}{" "} + - {investor.maximumInvestment || 0}

- +
- Total Investments -

{investor.totalInvestments} companies

+ + Total Investments + +

+ {investor.totalInvestments || 0} companies +

- +
- Typical Investment Timeline -

3-5 years

+ + Typical Investment Timeline + +

+ {investor.minTimline || 0}-{investor.maxTimline || 0} years +

- +
- Investment Focus + + Investment Focus +
-
- SaaS & B2B -
-
-
-
-
- FinTech -
-
-
-
-
- HealthTech -
-
-
-
+ {(investor.investmentInterests && + investor.investmentInterests.map((interest, index) => ( +
+ + {interest} + +
+
+
+
+ ))) || + "--"}
- + {/* Stats */} -

Investment Stats

+

+ Investment Stats +

-

Successful Exits

-

4

+

+ Successful Exits +

+

+ {investor.successfullExits || 0} +

- +
-

Avg. ROI

-

3.2x

+

+ Avg. ROI +

+

+ 3.2x +

- +
-

Active Investments

-

{investor.portfolioCompanies.length}

+

+ Active Investments +

+

+ {(investor.portfolioCompanies && + investor.portfolioCompanies.length) || + 0} +

@@ -269,4 +369,4 @@ export const InvestorProfile: React.FC = () => {
); -}; \ No newline at end of file +}; diff --git a/src/pages/settings/SettingsPage.tsx b/src/pages/settings/SettingsPage.tsx index bb6d5f17f..73d9bd1a2 100644 --- a/src/pages/settings/SettingsPage.tsx +++ b/src/pages/settings/SettingsPage.tsx @@ -1,174 +1,126 @@ -import React from 'react'; -import { User, Lock, Bell, Globe, Palette, CreditCard } from 'lucide-react'; -import { Card, CardHeader, CardBody } from '../../components/ui/Card'; -import { Input } from '../../components/ui/Input'; -import { Button } from '../../components/ui/Button'; -import { Badge } from '../../components/ui/Badge'; -import { Avatar } from '../../components/ui/Avatar'; -import { useAuth } from '../../context/AuthContext'; +import React, { useEffect, useState } from "react"; +import { User, Lock, Palette, CreditCard } from "lucide-react"; +import { Card, CardHeader, CardBody } from "../../components/ui/Card"; +import { useAuth } from "../../context/AuthContext"; +import { getEnterpreneurById, getInvestorById } from "../../data/users"; +import { Entrepreneur, Investor } from "../../types"; +import { ProfileSettings } from "../../components/settings/ProfileSettings"; +import { SecuritySettings } from "../../components/settings/SecuritySettings"; +import { AppearanceSettings } from "../../components/settings/AppearanceSettings"; +import { BillingSettings } from "../../components/settings/BillingSettings"; + +type SettingsTab = "profile" | "security" | "appearance" | "billing"; export const SettingsPage: React.FC = () => { - const { user } = useAuth(); - - if (!user) return null; - + const { user: currentUser } = useAuth(); + const [user, setUser] = useState(); + const [activeTab, setActiveTab] = useState("profile"); + + useEffect(() => { + const fetchUser = async () => { + let fetchedUser = null; + if (currentUser?.role === "investor") + fetchedUser = await getInvestorById(currentUser?.userId); + else if (currentUser?.role === "entrepreneur") + fetchedUser = await getEnterpreneurById(currentUser?.userId); + + setUser(fetchedUser); + }; + fetchUser(); + }, [currentUser]); + + const navItems = [ + { id: "profile" as SettingsTab, label: "Profile", icon: User }, + { id: "security" as SettingsTab, label: "Security", icon: Lock }, + { id: "appearance" as SettingsTab, label: "Appearance", icon: Palette }, + { id: "billing" as SettingsTab, label: "Billing", icon: CreditCard }, + ]; + + const renderContent = () => { + switch (activeTab) { + case "profile": + return ; + case "security": + return ; + case "appearance": + return ; + case "billing": + return ; + default: + return ; + } + }; + return (

Settings

-

Manage your account preferences and settings

+

+ Manage your account preferences and settings +

- +
- {/* Settings navigation */} - - + {/* Desktop Settings Navigation */} + + +

Settings

+
+
- - {/* Main settings content */} -
- {/* Profile Settings */} - - -

Profile Settings

-
- -
- - -
- -

- JPG, GIF or PNG. Max size of 800K -

-
-
- -
- - - - - - - -
- -
- - -
- -
- - -
-
-
- - {/* Security Settings */} - - -

Security Settings

-
- -
-

Two-Factor Authentication

-
-
-

- Add an extra layer of security to your account -

- Not Enabled -
- -
-
- -
-

Change Password

-
- - - - - - -
- -
-
-
+ + {/* Mobile Settings Navigation - Icon Only */} +
+ + +
+ + {/* Main settings content */} +
{renderContent()}
); diff --git a/src/pages/webRTC/AudioCall.tsx b/src/pages/webRTC/AudioCall.tsx new file mode 100644 index 000000000..beb701a75 --- /dev/null +++ b/src/pages/webRTC/AudioCall.tsx @@ -0,0 +1,235 @@ +import React, { useEffect, useRef, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { useAuth } from "../../context/AuthContext"; +import { useSocket } from "../../context/SocketContext"; +import toast from "react-hot-toast"; +import AgoraRTC, { ILocalTrack } from "agora-rtc-sdk-ng"; +import axios from "axios"; + +export const AudioCall: React.FC = () => { + const { roomId, userId, isIncommingCall } = useParams(); + const [isMute, setIsMute] = useState(false); + const { socket } = useSocket(); + const { user } = useAuth(); + const navigate = useNavigate(); + const APP_ID = import.meta.env.VITE_APP_ID; + const CHANNEL = String(roomId); + const [joined, setJoined] = useState(false); + const localAudioRef = useRef(null); + const remoteAudioRef = useRef(null); + const localTracksRef = useRef([]); + const clientRef = useRef(null); + const [token, setToken] = useState(null); + const [uid, setUid] = useState(null); + const URL = import.meta.env.VITE_BACKEND_URL; + + useEffect(() => { + const fetchToken = async () => { + const uid = String(Date.now()); + setUid(uid); + + try { + const res = await axios.get(`${URL}/agora/rtc/${CHANNEL}/${uid}`, { + withCredentials: true, + }); + setToken(res.data.token); + } catch (err) { + console.error("Failed to fetch token:", err); + } + }; + + fetchToken(); + }, [CHANNEL, URL]); + + useEffect(() => { + if (!token || !uid) return; + + AgoraRTC.setLogLevel(0); + + const init = async () => { + const client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" }); + clientRef.current = client; + + // 🔹 Set up listeners BEFORE joining/publishing + client.on("user-published", async (user, mediaType) => { + if (user.uid === uid) return; // skip own stream + await client.subscribe(user, mediaType); + console.log("Subscribed to:", user.uid); + + if (mediaType === "audio") { + user.audioTrack?.play(); + } + }); + + client.on("user-unpublished", (user, mediaType) => { + if (mediaType === "audio" && remoteAudioRef.current) { + remoteAudioRef.current.innerHTML = ""; + } + }); + + try { + await client.join(APP_ID, CHANNEL, token, uid); + + // create local tracks + localTracksRef.current = [await AgoraRTC.createMicrophoneAudioTrack()]; + + // publish AFTER listeners are ready + await client.publish(localTracksRef.current); + + localTracksRef.current[0].play(localAudioRef.current!); + setJoined(true); + } catch (err) { + console.error("Agora join error:", err); + } + + // Outgoing call + const isIncoming = isIncommingCall === "true"; + if (!isIncoming) { + socket?.emit("start-call", { + from: user?.userId, + to: userId, + roomId: CHANNEL, + callType: "audio", + fromName: user?.name, + }); + } + }; + + init(); + + return () => { + cleanup(); + }; + }, [CHANNEL, isIncommingCall,APP_ID, user, userId, token, uid, socket]); + + // === Cleanup === + const cleanup = async () => { + localTracksRef.current.forEach((track) => { + track.stop(); + track.close(); + }); + if (clientRef.current) { + clientRef.current.removeAllListeners(); // ✅ avoid duplicate handlers + await clientRef.current.leave(); + } + setJoined(false); + }; + + // Mute mic + const muteMic = async () => { + if (localTracksRef.current[0]) { + await localTracksRef.current[0].setEnabled(false); + setIsMute(true); + console.log("Microphone muted"); + } + }; + + // Unmute mic + const unmuteMic = async () => { + if (localTracksRef.current[0]) { + await localTracksRef.current[0].setEnabled(true); + + setIsMute(false); + console.log("Microphone unmuted"); + } + }; + + useEffect(() => { + socket?.on("call-accepted", () => { + toast.success("Call accepted"); + }); + + socket?.on("call-ended", () => { + toast.success("Call ended"); + navigate(`/chat/${userId}`); + }); + + socket?.on("call-rejected", () => { + toast.error("Your call is declined."); + navigate(`/chat/${userId}`); + }); + + socket?.on("receiver-offline", async () => { + toast.error("The receiver is offline."); + await cleanup(); + navigate(`/chat/${userId}`); + }); + + return () => { + socket?.off("call-accepted"); + socket?.off("receiver-offline"); + socket?.off("call-rejected"); + socket?.off("call-ended"); + }; + }, [socket, userId,navigate]); + return ( +
+

Agora 1-to-1 Audio Call

+
+
+
+
+

+ P +

+
+ {/* Local audio as overlay (picture-in-picture) */} +
+

+ U +

+
+
+
+ +
+ {joined ? ( +

📡 Call in Progress...

+ ) : ( +

Not connected

+ )} +
+ {/* End Call */} + + + {/* Mic Control */} + {isMute ? ( + + ) : ( + + )} +
+
+
+
+ ); +}; diff --git a/src/pages/webRTC/Videocall.tsx b/src/pages/webRTC/Videocall.tsx new file mode 100644 index 000000000..cb1414c6f --- /dev/null +++ b/src/pages/webRTC/Videocall.tsx @@ -0,0 +1,283 @@ +import React, { useEffect, useRef, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { useAuth } from "../../context/AuthContext"; +import { useSocket } from "../../context/SocketContext"; +import toast from "react-hot-toast"; +import AgoraRTC, { ILocalTrack } from "agora-rtc-sdk-ng"; +import axios from "axios"; + +export const VideoCall: React.FC = () => { + const { roomId, userId, isIncommingCall } = useParams(); + const [isMute, setIsMute] = useState(false); + const [isVideoOn, setIsVideoOn] = useState(false); + const { socket } = useSocket(); + const { user } = useAuth(); + const navigate = useNavigate(); + const APP_ID = "5e3db4d74aaa43ff8eeee3ad9f08efd8"; + const CHANNEL = String(roomId); + const [joined, setJoined] = useState(false); + const localVideoRef = useRef(null); + const remoteVideoRef = useRef(null); + const localTracksRef = useRef([]); + const clientRef = useRef(null); + const [token, setToken] = useState(null); + const [uid, setUid] = useState(null); + const URL = import.meta.env.VITE_BACKEND_URL; + + useEffect(() => { + const fetchToken = async () => { + // generate uid (string or number) + const uid = String(Date.now()); // simple unique uid + setUid(uid); + + try { + const res = await axios.get(`${URL}/agora/rtc/${CHANNEL}/${uid}`, { + withCredentials: true, + }); + setToken(res.data.token); + } catch (err) { + console.error("Failed to fetch token:", err); + } + }; + + fetchToken(); + }, [CHANNEL, URL]); + + useEffect(() => { + if (!token || !uid) return; + + AgoraRTC.setLogLevel(0); + + const init = async () => { + const client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" }); + clientRef.current = client; + + // 🔹 Set up listeners BEFORE joining/publishing + client.on("user-published", async (user, mediaType) => { + if (user.uid === uid) return; // skip own stream + await client.subscribe(user, mediaType); + console.log("Subscribed to:", user.uid); + + if (mediaType === "video") { + user.videoTrack?.play(remoteVideoRef.current!); + } + if (mediaType === "audio") { + user.audioTrack?.play(); + } + }); + + client.on("user-unpublished", (user, mediaType) => { + if (mediaType === "video" && remoteVideoRef.current) { + remoteVideoRef.current.innerHTML = ""; + } + }); + + try { + await client.join(APP_ID, CHANNEL, token, uid); + + // create local tracks + localTracksRef.current = + await AgoraRTC.createMicrophoneAndCameraTracks(); + + // publish AFTER listeners are ready + await client.publish(localTracksRef.current); + + localTracksRef.current[1].play(localVideoRef.current!); + setIsVideoOn(true); + setJoined(true); + } catch (err) { + console.error("Agora join error:", err); + } + + // Outgoing call + const isIncoming = isIncommingCall === "true"; + if (!isIncoming) { + socket?.emit("start-call", { + from: user?.userId, + to: userId, + roomId: CHANNEL, + callType:"video", + fromName:user?.name + }); + } + }; + + init(); + + return () => { + cleanup(); + }; + }, [CHANNEL, isIncommingCall, user, userId, token, uid, socket]); + + // === Cleanup === + const cleanup = async () => { + localTracksRef.current.forEach((track) => { + track.stop(); + track.close(); + }); + if (clientRef.current) { + clientRef.current.removeAllListeners(); // ✅ avoid duplicate handlers + await clientRef.current.leave(); + } + setJoined(false); + }; + + // Mute mic + const muteMic = async () => { + if (localTracksRef.current[0]) { + await localTracksRef.current[0].setEnabled(false); + setIsMute(true); + console.log("Microphone muted"); + } + }; + + // Unmute mic + const unmuteMic = async () => { + if (localTracksRef.current[0]) { + await localTracksRef.current[0].setEnabled(true); + + setIsMute(false); + console.log("Microphone unmuted"); + } + }; + + // Stop video + const stopVideo = async () => { + if (localTracksRef.current[1]) { + await localTracksRef.current[1].setEnabled(false); + + setIsVideoOn(false); + console.log("Video stopped"); + } + }; + + // Resume video + const resumeVideo = async () => { + if (localTracksRef.current[1]) { + await localTracksRef.current[1].setEnabled(true); + setIsVideoOn(true); + console.log("Video resumed"); + } + }; + + useEffect(() => { + socket?.on("call-accepted", () => { + toast.success("Call accepted"); + }); + + socket?.on("call-ended", () => { + toast.success("Call ended"); + navigate(`/chat/${userId}`); + }); + + socket?.on("call-rejected", () => { + toast.error("Your call is declined."); + navigate(`/chat/${userId}`); + }); + + socket?.on("receiver-offline", async () => { + toast.error("The receiver is offline."); + await cleanup(); + navigate(`/chat/${userId}`); + }); + + return () => { + socket?.off("call-accepted"); + socket?.off("receiver-offline"); + socket?.off("call-rejected"); + socket?.off("call-ended"); + }; + }, [socket, userId]); + return ( +
+

Agora 1-to-1 Video Call

+
+
+
+
+ {remoteVideoRef.current === null && ( +

+ O +

+ )} +
+ {/* Local video as overlay (picture-in-picture) */} +
+
+ {!isVideoOn && ( +

+ Q +

+ )} +
+
+
+ +
+ {joined ? ( +

📡 Call in Progress...

+ ) : ( +

Not connected

+ )} +
+ {/* End Call */} + + + {/* Mic Control */} + {isMute ? ( + + ) : ( + + )} + + {/* Video Control */} + {isVideoOn ? ( + + ) : ( + + )} +
+
+
+
+ ); +}; diff --git a/src/types/index.ts b/src/types/index.ts index 02212bbf9..4e0782d98 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,44 +1,48 @@ -export type UserRole = 'entrepreneur' | 'investor'; +export type UserRole = "entrepreneur" | "investor" | "admin"; export interface User { - id: string; + userId: string | undefined; name: string; email: string; role: UserRole; - avatarUrl: string; + avatarUrl: string | File | null; + location: string; bio: string; isOnline?: boolean; - createdAt: string; } export interface Entrepreneur extends User { - role: 'entrepreneur'; - startupName: string; - pitchSummary: string; - fundingNeeded: string; - industry: string; - location: string; - foundedYear: number; - teamSize: number; + startupName: string | undefined; + pitchSummary: string | undefined; + fundingNeeded: number | undefined; + industry: string | undefined; + foundedYear: number | undefined; + teamSize: number | undefined; + revenue: string | undefined; + profitMargin: number | undefined; + growthRate: number | undefined; + marketOpportunity: string | undefined; + advantage: string | undefined; } export interface Investor extends User { - role: 'investor'; - investmentInterests: string[]; - investmentStage: string[]; - portfolioCompanies: string[]; - totalInvestments: number; - minimumInvestment: string; - maximumInvestment: string; + investmentInterests: string[] | undefined; + portfolioCompanies: string[] | undefined; + totalInvestments: number | undefined; + minimumInvestment: number | undefined; + maximumInvestment: number | undefined; + investmentCriteria: string[] | undefined; + successfullExits: number | undefined; + minTimline: number | undefined; + maxTimline: number | undefined; } export interface Message { - id: string; - senderId: string; - receiverId: string; + sender: string; + receiver: string; content: string; - timestamp: string; isRead: boolean; + time: Date; } export interface ChatConversation { @@ -49,12 +53,11 @@ export interface ChatConversation { } export interface CollaborationRequest { - id: string; - investorId: string; - entrepreneurId: string; + inves_id: string; + enter_id: string; message: string; - status: 'pending' | 'accepted' | 'rejected'; - createdAt: string; + requestStatus: "pending" | "accepted" | "rejected"; + time: Date; } export interface Document { @@ -70,12 +73,23 @@ export interface Document { export interface AuthContextType { user: User | null; + userData: User | null; login: (email: string, password: string, role: UserRole) => Promise; - register: (name: string, email: string, password: string, role: UserRole) => Promise; + register: ( + name: string, + email: string, + password: string, + role: UserRole + ) => Promise; logout: () => void; forgotPassword: (email: string) => Promise; resetPassword: (token: string, newPassword: string) => Promise; updateProfile: (userId: string, updates: Partial) => Promise; + loginWithOauth: (userToken: string, role: UserRole) => Promise; isAuthenticated: boolean; isLoading: boolean; -} \ No newline at end of file +} + +export interface Socketcontext { + socket: string | null; +}