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..397de37aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,17 +8,27 @@ "name": "business-nexus", "version": "0.1.0", "dependencies": { - "axios": "^1.6.7", + "@hookform/resolvers": "^5.2.2", + "agora-rtc-sdk-ng": "^4.24.0", + "axios": "^1.13.2", "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-hook-form": "^7.67.0", + "react-hot-toast": "^2.6.0", + "react-is": "^19.2.0", + "react-router-dom": "^6.22.1", + "recharts": "^3.5.0", + "socket.io-client": "^4.8.1", + "yup": "^1.7.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 +44,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", @@ -86,6 +129,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.7.tgz", "integrity": "sha512-yJ474Zv3cwiSOO9nXJuqzvwEeM+chDuQ8GJirw+pZ91sCGCyOZ3dJkVE09fTV0VEVzXyLWhh3G/AolYTPX7Mow==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.25.7", @@ -722,7 +766,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 +780,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 +791,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 +799,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 +812,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 +820,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 +842,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 +853,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 +861,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 +869,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" }, @@ -842,11 +876,22 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanfs/core": { "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 +900,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 +912,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 +924,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 +1042,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 +1285,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 +1344,102 @@ "@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", + "peer": true, + "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, + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1295,6 +1454,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", @@ -1333,6 +1498,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.1.tgz", "integrity": "sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.8.1", "@typescript-eslint/types": "8.8.1", @@ -1548,7 +1714,7 @@ "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1560,16 +1726,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 +1820,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 +1875,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.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "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 +1906,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" @@ -1758,6 +1942,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001663", "electron-to-chromium": "^1.5.28", @@ -1788,7 +1973,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 +1987,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 +2004,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "2.4.2", @@ -1872,6 +2057,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 +2105,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 +2117,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", @@ -1949,7 +2141,129 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "peer": true + }, + "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", @@ -1965,7 +2279,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 +2291,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 +2355,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 +2422,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 +2492,7 @@ "version": "9.12.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.12.0.tgz", "integrity": "sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==", - "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", @@ -2210,6 +2560,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 +2585,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 +2600,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 +2611,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 +2625,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 +2640,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 +2650,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 +2667,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 +2675,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 +2686,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 +2702,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 +2713,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 +2724,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 +2732,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 +2778,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 +2794,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 +2856,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 +2871,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 +2882,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 +2921,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 +2961,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 +3080,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 +3201,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 +3234,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 +3278,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 +3295,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 +3314,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 +3335,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 +3349,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 +3371,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 +3395,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 +3416,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 +3443,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 +3456,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 +3542,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 +3558,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 +3610,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 +3677,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 +3693,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 +3707,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 +3723,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 +3744,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 +3752,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" } @@ -3384,6 +3839,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.0", @@ -3524,7 +3980,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" } @@ -3540,6 +3995,18 @@ "react-is": "^16.13.1" } }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -3550,7 +4017,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" } @@ -3579,6 +4045,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3590,6 +4057,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -3615,10 +4083,27 @@ "react": ">= 16.8 || 18.0.0" } }, + "node_modules/react-hook-form": { + "version": "7.67.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.67.0.tgz", + "integrity": "sha512-E55EOwKJHHIT/I6J9DmQbCWToAYSw9nN5R57MZw9rMtjh+YQreMDxRLfdjfxQbiJ3/qbg3Z02wGzBX4M+5fMtQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "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", @@ -3633,10 +4118,35 @@ } }, "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", + "license": "MIT", + "peer": true + }, + "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", + "peer": true, + "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", @@ -3700,6 +4210,59 @@ "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", + "peer": true + }, + "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 +4284,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 +4364,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 +4383,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 +4394,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 +4410,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 +4547,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 +4640,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 +4663,18 @@ "node": ">=0.8" } }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "license": "MIT" + }, + "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", @@ -4092,6 +4696,12 @@ "node": ">=8.0" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -4120,7 +4730,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" }, @@ -4128,11 +4737,24 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4164,6 +4786,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 +4853,54 @@ "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", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -4268,11 +4955,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 +4995,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 +5120,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,13 +5171,24 @@ "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" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yup": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz", + "integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==", + "license": "MIT", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } } } } diff --git a/package.json b/package.json index cdfd37b29..1a16763ad 100644 --- a/package.json +++ b/package.json @@ -10,17 +10,27 @@ "preview": "vite preview" }, "dependencies": { + "@hookform/resolvers": "^5.2.2", + "agora-rtc-sdk-ng": "^4.24.0", + "axios": "^1.13.2", + "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-hook-form": "^7.67.0", + "react-hot-toast": "^2.6.0", + "react-is": "^19.2.0", + "react-router-dom": "^6.22.1", + "socket.io-client": "^4.8.1", + "yup": "^1.7.1", +"recharts": "^3.5.0" }, "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 +44,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..4ef1e0588 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,104 +1,160 @@ -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"; // 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 { 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"; +import { AICommandPage } from "./pages/admin/AI"; +import {Campaigns }from "./pages/admin/Campaigns"; +import { Deals } from "./pages/viewdeals/ViewDeal"; +import { DealsPage } from "./pages/deals/DealsPage"; 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..cdd4c1681 --- /dev/null +++ b/src/components/camp/CampForm.tsx @@ -0,0 +1,248 @@ +"use client"; + +import React, { useState } from "react"; +import axios from "axios"; +import toast from "react-hot-toast"; + +interface CampFormProps { + onSuccess: () => void; +} + +const CampForm: React.FC = ({ onSuccess }) => { + const [formData, setFormData] = useState({ + title: "", + description: "", + goalAmount: "", + startDate: "", + endDate: "", + category: "Other", + }); + + const [images, setImages] = useState([]); + const [previewUrls, setPreviewUrls] = useState([]); + const [loading, setLoading] = useState(false); + const fileInputRef = useRef(null); + + const handleChange = ( + e: React.ChangeEvent + ) => { + const { name, value } = e.target; + + // Title: only letters and spaces + if (name === "title" && !/^[A-Za-z\s]*$/.test(value)) return; + + // Description: letters, numbers, and spaces only + if (name === "description" && !/^[A-Za-z0-9\s]*$/.test(value)) return; + + // Category: only letters and spaces + if (name === "category" && !/^[A-Za-z\s]*$/.test(value)) return; + + // Goal amount: only numbers + if (name === "goalAmount" && !/^[0-9]*$/.test(value)) return; + + setFormData({ ...formData, [name]: value }); + }; + + const handleFileChange = (e: React.ChangeEvent) => { + if (e.target.files) { + const newFiles = Array.from(e.target.files); + setImages((prev) => [...prev, ...newFiles]); + setPreviewUrls((prev) => [...prev, ...newFiles.map((file) => URL.createObjectURL(file))]); + } + }; + + const removeImage = (index: number) => { + setImages((prev) => prev.filter((_, i) => i !== index)); + setPreviewUrls((prev) => prev.filter((_, i) => i !== index)); + if (fileInputRef.current) { + fileInputRef.current.value = ""; // reset file input + } + }; + + const validateForm = () => { + const { title, description, goalAmount, startDate, endDate, category } = formData; + + if (!title.trim()) { + toast.error("Title is required"); + return false; + } + if (!/^[A-Za-z\s]+$/.test(title)) { + toast.error("Title must contain only letters"); + return false; + } + + if (!description.trim()) { + toast.error("Description is required"); + return false; + } + if (!/^[A-Za-z0-9\s]+$/.test(description)) { + toast.error("Description can contain only letters, numbers, and spaces"); + return false; + } + + const wordCount = description.trim().split(/\s+/).length; + if (wordCount < 30) { + toast.error("Description must be at least 30 words"); + return false; + } + + if (!goalAmount || isNaN(Number(goalAmount)) || Number(goalAmount) <= 0) { + toast.error("Goal amount must be a positive number"); + return false; + } + + 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; + } + + if (!category.trim()) { + toast.error("Category is required"); + return false; + } + + if (images.length === 0) { + toast.error("At least one image is required"); + 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(([k, v]) => data.append(k, v)); + images.forEach((file) => data.append("images", file)); + + await axios.post("http://localhost:5000/admin/campaigns", data); + toast.success("Campaign created successfully!"); + + setFormData({ + title: "", + description: "", + goalAmount: "", + startDate: "", + endDate: "", + category: "Other", + }); + setImages([]); + setPreviewUrls([]); + + onSuccess(); + } catch (err: any) { + toast.error(err.response?.data?.message || "Failed to create campaign"); + } finally { + setLoading(false); + } + }; + + return ( +
+ + + + +
+ +
+ + + + {/* 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..5e97f25c9 --- /dev/null +++ b/src/components/settings/SecuritySettings.tsx @@ -0,0 +1,298 @@ +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! +

+
+ )} + + + + + + + +
+ +
+
+
+
+ ); +}; 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/AI.tsx b/src/pages/admin/AI.tsx new file mode 100644 index 000000000..1dfe7eb19 --- /dev/null +++ b/src/pages/admin/AI.tsx @@ -0,0 +1,50 @@ +import { useState } from "react"; +import { Input } from "../../components/ui/Input"; +import { Button } from "../../components/ui/Button"; +import { Card, CardBody, CardHeader } from "../../components/ui/Card"; +import { useAuth } from "../../context/AuthContext"; + +export const AICommandPage = () => { + const { user } = useAuth(); + + const [command, setCommand] = useState(""); + const [modalText, setModalText] = useState(""); + + const handleSend = () => { + if (!command.trim()) return; + + // Here you can later call your API: await axios.post("/api/ai", { command }) + setModalText(command); + }; + + return ( +
+

+ Take the Suggestions as a {user?.role.toUpperCase()} +

+ +
+ setCommand(e.target.value)} + className="text-lg pt-4" + /> + +
+ +
+
+ + {/* MODAL */} + + AI Command Output + +

{modalText}

+
+
+
+ ); +}; diff --git a/src/pages/admin/Campaigns.tsx b/src/pages/admin/Campaigns.tsx new file mode 100644 index 000000000..b9c6fb42a --- /dev/null +++ b/src/pages/admin/Campaigns.tsx @@ -0,0 +1,268 @@ +"use client"; +import React, { useEffect, useState, useRef } from "react"; +import axios from "axios"; +import toast from "react-hot-toast"; +import CampForm from "../../components/camp/CampForm"; + +interface Campaign { + _id: string; + title: string; + description: string; + goalAmount: number; + raisedAmount: number; + status: string; + category: string; + startDate: string; + endDate: string; + images?: string[]; +} + +export 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("http://localhost:5000/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(`http://localhost:5000/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

+ +
+ +
+ + { + setQuery(e.target.value); + if (e.target.value === "") setFiltered(campaigns); + }} + className="flex-grow px-4 py-2 bg-transparent focus:outline-none text-gray-700 placeholder-gray-400" + /> + + +
+ +
+
+

Total Campaigns

+

{total}

+
+
+

Active

+

{active}

+
+
+

Stopped

+

{stopped}

+
+
+ + {showForm && ( +
+
+ +
+ { + fetchCampaigns(); + setShowForm(false); + }} + /> +
+
+
+ )} + + {filtered.length > 6 && ( +
+ + +
+ )} + +
+ {filtered.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} +

+
+ +

+ Category: {c.category} +

+ +
+

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

+

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

+
+ +

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

+ +
+ {c.status === "active" ? ( + + ) : ( + + )} +
+
+ ))} +
+
+ ); +}; + diff --git a/src/pages/admin/FraudAndRiskDetection.tsx b/src/pages/admin/FraudAndRiskDetection.tsx new file mode 100644 index 000000000..109660ffb --- /dev/null +++ b/src/pages/admin/FraudAndRiskDetection.tsx @@ -0,0 +1,113 @@ +import { useEffect, useState } from "react"; +import axios from "axios"; +import { FraudAndRiskDetectionChart } from "../../components/admin/FraudAndRiskDetectionChart"; + +interface SummaryItem { + eventType: string; + email: string; + count: number; +} + +interface FinalItem { + eventType: string; + riskScore: number; + count: number; +} + +export const FraudAndRiskDetection: React.FC = () => { + const [summary, setSummary] = useState([]); + const [finalData, setFinalData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + try { + const res = await axios.get( + `${import.meta.env.VITE_BACKEND_URL}/admin/risk-detection-flags` + ); + setSummary(res.data.summary); + setFinalData(res.data.finalData); + } catch (err) { + console.error("Error fetching risk flags:", err); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + if (loading) return
Loading risk data…
; + + return ( +
+ {/* Header */} +
+
+

Fraud & Risk Detection

+

+ This dashboard helps you monitor suspicious user activities, detect + abnormal login patterns, and track high-risk events happening within + the system. It summarizes flagged behaviors based on event types, + email usage, and risk scores recorded in the past 12 months. +

+
+
+ +
+
+ + {/* Summary Table */} +
+

Event Summary

+
+ + + + + + + + + + {summary.map((item, idx) => ( + + + + + + ))} + +
Event TypeEmailTotal Events
{item.eventType}{item.email}{item.count}
+
+
+ + {/* Final Data Table */} +
+

+ Risk Score Breakdown (Last 12 Months) +

+
+ + + + + + + + + + {finalData.map((item, idx) => ( + + + + + + ))} + +
Event TypeRisk ScoreEvent Count
{item.eventType}{item.riskScore}{item.count}
+
+
+
+ ); +}; diff --git a/src/pages/admin/Users.tsx b/src/pages/admin/Users.tsx new file mode 100644 index 000000000..0e791ece9 --- /dev/null +++ b/src/pages/admin/Users.tsx @@ -0,0 +1,436 @@ +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()) || + ent.email.toLowerCase().includes(query.toLowerCase()) || + ent.role.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/flaggedAccounts.tsx b/src/pages/admin/flaggedAccounts.tsx new file mode 100644 index 000000000..cbc6ba3a0 --- /dev/null +++ b/src/pages/admin/flaggedAccounts.tsx @@ -0,0 +1,151 @@ +"use client"; +import React, { useEffect, useRef, useState } from "react"; +import { Button } from "../../components/ui/Button"; +import { ThreeDotsButton } from "../../components/ui/ThreeDotsButton"; + +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", + }, + ]; + + type UserData = { + userId: string; + role: string; + }; + + const [showDialog, setShowDialog] = useState(false); + const [index, setIndex] = useState(null); + 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); + }, []); + + return ( +
+

Flagged / Blocked Accounts

+ +
+ + + + + + + + + + + + + {flaggedUsers.map((user, idx) => ( + + + + + + + + ))} + +
NameEmailRoleReasonStatus
{user.name}{user.email}{user.role}{user.reason} +
+ + {user.status.toUpperCase()} + + { + e.stopPropagation(); + setIndex(idx); + setShowDialog((prev) => !prev); + }} + /> +
+ + {showDialog && idx === index && ( +
= flaggedUsers.length - 2 + ? "bottom-full mb-2" + : "top-full mt-2" + } + `} + > + {user.status === "blocked" && ( + + )} + {user.status === "suspended" && ( + + )} +
+ )} +
+
+
+ ); +}; 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..544b40c53 --- /dev/null +++ b/src/pages/admin/supporters.tsx @@ -0,0 +1,202 @@ +import React, { useEffect, useRef, useState } from "react"; +import { Input } from "../../components/ui/Input"; +import { SearchIcon, Trash } from "lucide-react"; +import { Button } from "../../components/ui/Button"; +import { ThreeDotsButton } from "../../components/ui/ThreeDotsButton"; + +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 [showDialog, setShowDialog] = useState(false); + const [index, setIndex] = useState(null); + const dialogRef = useRef(null); + + // close on outside click + useEffect(() => { + function handleClickOutside(e) { + if (dialogRef.current && !dialogRef.current.contains(e.target)) { + setShowDialog(false); + setIndex(null); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const TableRow = ({ sup, idx }) => { + return ( + + {sup.name} + {sup.email} + {sup.campaign} + + + + ${sup.amount.toLocaleString()} + + + {/* 3-dots button */} + { + e.stopPropagation(); + setIndex(idx); + setShowDialog((prev) => !prev); + }} + /> + + {/* dropdown menu */} + {showDialog && index === idx && ( +
+ + + +
+ )} + + + ); + }; + + 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..379dedc58 100644 --- a/src/pages/auth/ForgotPasswordPage.tsx +++ b/src/pages/auth/ForgotPasswordPage.tsx @@ -4,86 +4,116 @@ import { Mail, ArrowLeft } from 'lucide-react'; import { useAuth } from '../../context/AuthContext'; import { Button } from '../../components/ui/Button'; import { Input } from '../../components/ui/Input'; +import toast from 'react-hot-toast'; export const ForgotPasswordPage: React.FC = () => { const [email, setEmail] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false); - + const { forgotPassword } = useAuth(); - + + // Allowed common email domains + const allowedDomains = [ + 'gmail.com', + 'yahoo.com', + 'outlook.com', + 'icloud.com', + 'hotmail.com', + 'aol.com', + 'live.com', + 'msn.com', + 'protonmail.com', + ]; + + const validateEmail = (email: string) => { + if (!email.trim()) { + toast.error('Email is required'); + return false; + } + + // Basic email regex + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + toast.error('Invalid email format'); + return false; + } + + // Check domain + const domain = email.split('@')[1].toLowerCase(); + if (!allowedDomains.includes(domain)) { + toast.error(`Email domain must be one of: ${allowedDomains.join(', ')}`); + return false; + } + + return true; + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + + if (!validateEmail(email)) return; + setIsLoading(true); - try { await forgotPassword(email); setIsSubmitted(true); - } catch (error) { - // Error is handled by the AuthContext + toast.success(`Password reset instructions sent to ${email}`); + } catch (err: any) { + toast.error(err.response?.data?.message || 'Failed to send reset instructions'); } finally { setIsLoading(false); } }; - + if (isSubmitted) { return ( -
-
+
+
-

+

Check your email

-

+

We've sent password reset instructions to {email}

- -
-
-

- Didn't receive the email? Check your spam folder or try again. -

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

+

Forgot your password?

-

+

Enter your email address and we'll send you instructions to reset your password.

- -
+ +
{ fullWidth startAdornment={} /> - - - + - @@ -117,4 +139,4 @@ export const ForgotPasswordPage: React.FC = () => {
); -}; \ No newline at end of file +}; diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index bbd5b08a0..f44f40087 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -1,110 +1,140 @@ -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@trust.com"); + setPassword("admin123"); } 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

-
-
+
+
{error && (
{error}
)} - +
-
+ + {/* Responsive grid: stacks on small screens */} +
- + + +
- + { fullWidth startAdornment={} /> - + { required fullWidth /> - -
+ +
{ type="checkbox" className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded" /> -
- + + Forgot your password? +
- + - + + {/* --- Demo Accounts --- */}
@@ -163,26 +198,35 @@ export const LoginPage: React.FC = () => { Demo Accounts
- -
+ +
- + + +
- + + {/* Divider */}
@@ -192,14 +236,42 @@ export const LoginPage: React.FC = () => { Or
- -
-

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

+ +

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

+ +
+
+
+
+
+ Or +
+
+ + {/* OAuth */} +
+ +
diff --git a/src/pages/auth/LoginWithOAuthPage.tsx b/src/pages/auth/LoginWithOAuthPage.tsx new file mode 100644 index 000000000..d9098aaa3 --- /dev/null +++ b/src/pages/auth/LoginWithOAuthPage.tsx @@ -0,0 +1,114 @@ +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"); + + useEffect(() => { + if (!token) alert("Missing token"); + }, [token]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!token) { + alert("Missing token"); + return; + } + + await loginWithOauth(token, role); + navigate( + role === "entrepreneur" ? "/dashboard/entrepreneur" : "/dashboard/investor" + ); + }; + + return ( +
+
+
+
+
+ + + + +
+
+ +
+ + +
+ + + +
+
+ +
+ +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/src/pages/auth/RegisterPage.tsx b/src/pages/auth/RegisterPage.tsx index e6ff2ab89..02fd2627a 100644 --- a/src/pages/auth/RegisterPage.tsx +++ b/src/pages/auth/RegisterPage.tsx @@ -1,107 +1,187 @@ -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 [termsAccepted, setTermsAccepted] = useState(false); + const { register } = useAuth(); const navigate = useNavigate(); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(null); - - // Validate passwords match + + const validateForm = () => { + // Name validation: letters and spaces only + if (!name.trim()) { + setError("Full name is required"); + return false; + } + if (!/^[A-Za-z\s]+$/.test(name)) { + setError("Name can only contain letters and spaces"); + return false; + } + + // Email validation + if (!email.trim()) { + setError("Email is required"); + return false; + } + // Common email domains allowed +const emailRegex = /^[a-zA-Z0-9._%+-]+@(gmail|yahoo|icloud|outlook|hotmail|aol|protonmail|live|msn|comcast)\.com$/; +if (!emailRegex.test(email)) { + setError("Invalid Email"); + return false; +} + + + // Password validation + if (!password) { + setError("Password is required"); + return false; + } + const passwordRegex = + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; + if (!passwordRegex.test(password)) { + setError( + "Password must be at least 8 characters, include uppercase, lowercase, number and special character" + ); + return false; + } + + // Confirm password if (password !== confirmPassword) { - setError('Passwords do not match'); - return; + setError("Passwords do not match"); + return false; + } + + // Role validation + if (!role) { + setError("Select a role"); + return false; + } + + // Terms validation + if (!termsAccepted) { + setError("You must accept the Terms of Service and Privacy Policy"); + return false; } - + + // All good + setError(null); + return true; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!validateForm()) return; + setIsLoading(true); - try { - await register(name, email, password, role); - // Redirect based on user role - navigate(role === 'entrepreneur' ? '/dashboard/entrepreneur' : '/dashboard/investor'); - } catch (err) { - setError((err as Error).message); + // Save to local storage for demo purposes + localStorage.setItem( + "userInfo", + JSON.stringify({ name, email, password, role }) + ); + navigate("/fill-details"); + } catch (err: any) { + setError(err.message || "Something went wrong"); + setIsLoading(false); + } finally { setIsLoading(false); } }; - + return ( -
-
+
+
-
- - - - +
+
-

+

Create your account

-

- Join Business Nexus to connect with partners +

+ Join TrustBridge AI to connect with partners

-
-
+
+
{error && (
{error}
)} - +
+ {/* Role selection */}
-
+
- + + +
- + + {/* Name */} { fullWidth startAdornment={} /> - + + {/* Email */} { fullWidth startAdornment={} /> - + + {/* Password */} { fullWidth startAdornment={} /> - + + {/* Confirm Password */} { fullWidth startAdornment={} /> - -
+ + {/* Terms checkbox */} +
setTermsAccepted(e.target.checked)} required className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded" /> -
- - - +
@@ -180,11 +268,14 @@ export const RegisterPage: React.FC = () => { Or
- +

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

@@ -194,4 +285,4 @@ export const RegisterPage: React.FC = () => {
); -}; \ No newline at end of file +}; diff --git a/src/pages/auth/ResetPasswordPage.tsx b/src/pages/auth/ResetPasswordPage.tsx index ec64920fb..009bca244 100644 --- a/src/pages/auth/ResetPasswordPage.tsx +++ b/src/pages/auth/ResetPasswordPage.tsx @@ -1,9 +1,10 @@ import React, { useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; -import { Lock } from 'lucide-react'; +import { Lock, AlertCircle } from 'lucide-react'; import { useAuth } from '../../context/AuthContext'; import { Button } from '../../components/ui/Button'; import { Input } from '../../components/ui/Input'; +import toast from 'react-hot-toast'; export const ResetPasswordPage: React.FC = () => { const [password, setPassword] = useState(''); @@ -11,70 +12,92 @@ export const ResetPasswordPage: React.FC = () => { const [isLoading, setIsLoading] = useState(false); const [searchParams] = useSearchParams(); const navigate = useNavigate(); - const { resetPassword } = useAuth(); const token = searchParams.get('token'); - + + // Password validation function + const validatePassword = (pwd: string) => { + if (pwd.length < 8) { + toast.error('Password must be at least 8 characters long'); + return false; + } + if (!/[A-Z]/.test(pwd)) { + toast.error('Password must contain at least one uppercase letter'); + return false; + } + if (!/[a-z]/.test(pwd)) { + toast.error('Password must contain at least one lowercase letter'); + return false; + } + if (!/[0-9]/.test(pwd)) { + toast.error('Password must contain at least one number'); + return false; + } + if (!/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+/.test(pwd)) { + toast.error('Password must contain at least one special character'); + return false; + } + return true; + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - - if (!token) { - return; - } - + + if (!token) return; + + // Validate password strength + if (!validatePassword(password)) return; + + // Confirm password match if (password !== confirmPassword) { + toast.error('Passwords do not match'); return; } - + setIsLoading(true); - try { await resetPassword(token, password); + toast.success('Password reset successfully!'); navigate('/login'); - } catch (error) { - // Error is handled by the AuthContext + } catch (err: any) { + toast.error(err.response?.data?.message || 'Failed to reset password'); } finally { setIsLoading(false); } }; - + if (!token) { return ( -
-
-
-

- Invalid reset link -

-

- This password reset link is invalid or has expired. -

- -
+
+
+

+ Invalid reset link +

+

+ This password reset link is invalid or has expired. +

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

+

Reset your password

-

+

Enter your new password below

- -
+ +
{ required fullWidth startAdornment={} + helperText="Must be at least 8 characters with uppercase, lowercase, number & special char" /> - + { startAdornment={} error={password !== confirmPassword ? 'Passwords do not match' : undefined} /> - - @@ -109,4 +129,4 @@ export const ResetPasswordPage: 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..8776f6b0d --- /dev/null +++ b/src/pages/dashboard/AdminDashboard.tsx @@ -0,0 +1,300 @@ +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, useNavigate } 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, + }); + const navigate = useNavigate(); + + 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..b00ce248c 100644 --- a/src/pages/dashboard/InvestorDashboard.tsx +++ b/src/pages/dashboard/InvestorDashboard.tsx @@ -1,72 +1,104 @@ -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 [entrepreneurs, setEnterprenuers] = useState([]); + const [sentRequests, setSentRequests] = 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); + + 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 === '' || - entrepreneur.name.toLowerCase().includes(searchQuery.toLowerCase()) || - entrepreneur.startupName.toLowerCase().includes(searchQuery.toLowerCase()) || - entrepreneur.industry.toLowerCase().includes(searchQuery.toLowerCase()) || - entrepreneur.pitchSummary.toLowerCase().includes(searchQuery.toLowerCase()); - + 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()); + // Industry filter - const matchesIndustry = selectedIndustries.length === 0 || - selectedIndustries.includes(entrepreneur.industry); - + const matchesIndustry = + selectedIndustries.length === 0 || + selectedIndustries.includes(entrepreneur?.industry); + return matchesSearch && matchesIndustry; }); - + const industries = entrepreneurs + ? [...new Set(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 +110,21 @@ export const InvestorDashboard: React.FC = () => { startAdornment={} />
- +
-
+
- Filter by: - -
- {industries.map(industry => ( + + Filter by: + + +
+ {industries.map((industry) => ( toggleIndustry(industry)} > @@ -99,7 +135,7 @@ export const InvestorDashboard: React.FC = () => {
- + {/* Stats summary */}
@@ -109,13 +145,17 @@ export const InvestorDashboard: React.FC = () => {
-

Total Startups

-

{entrepreneurs.length}

+

+ Total Startups +

+

+ {entrepreneurs && entrepreneurs.length} +

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

Industries

-

{industries.length}

+

+ Industries +

+

+ {industries.length} +

- +
@@ -137,51 +181,59 @@ 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 ? ( -
- {filteredEntrepreneurs.map(entrepreneur => ( - - ))} -
- ) : ( -
-

No startups match your filters

- -
- )} +
+ {filteredEntrepreneurs.length > 0 ? ( + filteredEntrepreneurs.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..287562ea8 100644 --- a/src/pages/deals/DealsPage.tsx +++ b/src/pages/deals/DealsPage.tsx @@ -1,156 +1,182 @@ -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, +} 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 +188,19 @@ export const DealsPage: React.FC = () => { fullWidth />
- +
- {statuses.map(status => ( + {statuses.map((status) => ( toggleStatus(status)} > @@ -181,92 +211,98 @@ 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..d682db52c 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/help/HelpPage.tsx b/src/pages/help/HelpPage.tsx index e2b76fb85..48345e4c9 100644 --- a/src/pages/help/HelpPage.tsx +++ b/src/pages/help/HelpPage.tsx @@ -1,8 +1,9 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Search, Book, MessageCircle, Phone, Mail, ExternalLink } from 'lucide-react'; import { Card, CardHeader, CardBody } from '../../components/ui/Card'; import { Input } from '../../components/ui/Input'; import { Button } from '../../components/ui/Button'; +import toast from 'react-hot-toast'; const faqs = [ { @@ -24,13 +25,79 @@ const faqs = [ ]; export const HelpPage: React.FC = () => { + const [formData, setFormData] = useState({ + name: '', + email: '', + message: '' + }); + + // Only allow letters and spaces in name dynamically + const handleNameChange = (e: React.ChangeEvent) => { + const value = e.target.value; + if (/^[A-Za-z\s]*$/.test(value)) { + setFormData(prev => ({ ...prev, name: value })); + } + }; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + if (name === 'name') return; // Name handled separately + setFormData(prev => ({ ...prev, [name]: value })); + }; + + const validateForm = () => { + const { name, email, message } = formData; + + if (!name.trim()) { + toast.error('Name is required'); + return false; + } + if (!/^[A-Za-z\s]+$/.test(name)) { + toast.error('Name can only contain letters and spaces'); + return false; + } + + if (!email.trim()) { + toast.error('Email is required'); + return false; + } + // Only allow common domains like Gmail, Yahoo, Hotmail, Outlook + const allowedDomains = ['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com' , "icloud.com"]; + const emailRegex = new RegExp(`^[^\\s@]+@(${allowedDomains.join('|')})$`, 'i'); + if (!emailRegex.test(email)) { + toast.error('Inavlud Email'); + return false; + } + + if (!message.trim()) { + toast.error('Message is required'); + return false; + } + if (message.trim().length < 10) { + toast.error('Message must be at least 10 characters'); + return false; + } + + return true; + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!validateForm()) return; + + console.log('Form submitted', formData); + toast.success('Message sent successfully!'); + setFormData({ name: '', email: '', message: '' }); + }; + return (
+ {/* Header */}

Help & Support

Find answers to common questions or get in touch with our support team

- + {/* Search */}
{ fullWidth />
- + + {/* Quick links */}
- {/* Quick links */}
@@ -60,7 +127,7 @@ export const HelpPage: React.FC = () => { - +
@@ -75,7 +142,7 @@ export const HelpPage: React.FC = () => { - +
@@ -95,7 +162,7 @@ export const HelpPage: React.FC = () => {
- + {/* FAQs */} @@ -116,40 +183,49 @@ export const HelpPage: React.FC = () => {
- + {/* Contact form */}

Still need help?

- +
- +
- +
- +
-
@@ -158,4 +234,4 @@ export const HelpPage: React.FC = () => {
); -}; \ No newline at end of file +}; diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx new file mode 100644 index 000000000..e71ad50bc --- /dev/null +++ b/src/pages/home/HomePage.tsx @@ -0,0 +1,242 @@ +import React from "react"; +import { Navbar } from "../../components/home/Navbar"; +import { Button } from "../../components/ui/Button"; + +interface CampaignProps { + image: string; + title: string; + description: string; + goalAmount: 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 CampaignDiv: React.FC = ({ + image, + title, + description, + goalAmount, + }) => { + return ( +
+ pic +
+ +
+

+ Title: {title} +

+

Description: {description}

+ +

+ Goal Amount: ${goalAmount} +

+ +
+
+
+ ); + }; + + const FundraiserDiv: 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 ( +
+ + +
+ + +
+ +
+
+ {/* LEFT SIDE — Recent Campaigns */} +
+

+ Recent Campaigns.. +

+ +
+ + + +
+
+ + {/* MID CONTENT */} +
+
+

+ The platform where Entrepreneurs and Investors collaborate, + make deals and build successful ventures. +

+ +
+ +
+
+
+ + {/* RIGHT SIDE — Fundraisers */} +
+

+ Fundraisers.. +

+ +
+ + + +
+
+
+ + {/* SUCCESSFUL ENTREPRENEURS */} +
+

+ 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..35fc1b793 100644 --- a/src/pages/profile/EntrepreneurProfile.tsx +++ b/src/pages/profile/EntrepreneurProfile.tsx @@ -1,56 +1,123 @@ -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 [isDealModalOpen, setIsDealModalOpen] = useState(false); + + 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 +128,171 @@ 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 && ( - + + + {isInvestor && ( + + )} + + ) + ) : ( + <> + )} + + {hasRequestedCollaboration && ( + + )} + {isDealModalOpen && ( +
+
+

Make a Deal

+ + { + e.preventDefault(); + alert("Deal successfully submitted"); + + setIsDealModalOpen(false); + }} > - {hasRequestedCollaboration ? 'Request Sent' : 'Request Collaboration'} - - )} - + {/* Entrepreneur Info */} +
+ + +
+ +
+ + +
+ +
+ + -
- -
- - -
- - - - {/* 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()}
); -}; \ No newline at end of file +}; diff --git a/src/pages/viewdeals/ViewDeal.tsx b/src/pages/viewdeals/ViewDeal.tsx new file mode 100644 index 000000000..cacd86e77 --- /dev/null +++ b/src/pages/viewdeals/ViewDeal.tsx @@ -0,0 +1,83 @@ +"use client"; +import React, { useState } from "react"; +import { Card, CardBody, CardHeader } from "../../components/ui/Card"; +import { Button } from "../../components/ui/Button"; +import { toast } from "react-hot-toast"; + +export const Deals: React.FC = () => { + // Dummy static deals data + const [deals, setDeals] = useState([ + { + _id: "1", + investorName: "John Doe", + investorEmail: "john@example.com", + businessName: "Acme Startup", + amount: 50000, + equity: 5, + message: "We want to invest in your startup.", + status: "pending", + }, + { + _id: "2", + investorName: "Jane Smith", + investorEmail: "jane@example.com", + businessName: "Acme Startup", + amount: 100000, + equity: 10, + message: "We are interested in funding your growth.", + status: "pending", + }, + ]); + + const handleDealStatus = (dealId: string, status: "accepted" | "rejected") => { + setDeals(prev => prev.map(d => d._id === dealId ? { ...d, status } : d)); + toast.success(`Deal ${status}`); + }; + + return ( +
+

Investor Deals

+ + {deals.length === 0 ? ( +

No deals found yet.

+ ) : ( +
+ {deals.map(deal => ( + + +
+

{deal.investorName}

+

{deal.investorEmail}

+
+ + {deal.status} + +
+ +

Business: {deal.businessName}

+

Investment Amount: ${deal.amount}

+

Requested Equity: {deal.equity}%

+

Message: {deal.message}

+ + {deal.status === "pending" && ( +
+ + +
+ )} +
+
+ ))} +
+ )} +
+ ); +}; 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; +}