diff --git a/README.md b/README.md index 51f10c3..f96e4cd 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ A collaborative platform for developers to manage projects, tasks, discussions and team communication + ``` Developer-Collaboration-Platform ├─ client @@ -17,6 +18,10 @@ Developer-Collaboration-Platform │ │ ├─ App.css │ │ ├─ App.jsx │ │ ├─ assets +│ │ │ ├─ eyeOff.svg +│ │ │ ├─ eyeOn.svg +│ │ │ ├─ github.svg +│ │ │ ├─ google.svg │ │ │ ├─ hero.png │ │ │ ├─ react.svg │ │ │ └─ vite.svg @@ -28,26 +33,47 @@ Developer-Collaboration-Platform │ │ ├─ index.css │ │ ├─ main.jsx │ │ ├─ pages +│ │ │ ├─ dashboard.jsx +│ │ │ ├─ forgotpassword.jsx +│ │ │ ├─ login.jsx +│ │ │ └─ signup.jsx │ │ ├─ services +│ │ │ └─ googleAuthService.js │ │ ├─ styles │ │ └─ utils │ └─ vite.config.js -├─ docs ├─ LICENSE +├─ package-lock.json +├─ package.json ├─ README.md └─ server ├─ package-lock.json ├─ package.json ├─ prisma + │ ├─ migrations + │ │ ├─ 20260708161847_init + │ │ │ └─ migration.sql + │ │ ├─ 20260713130332_add_google_oauth + │ │ │ └─ migration.sql + │ │ ├─ 20260713132054_google_oauth + │ │ │ └─ migration.sql + │ │ ├─ 20260716153106_add_profile_completed + │ │ │ └─ migration.sql + │ │ └─ migration_lock.toml │ └─ schema.prisma - ├─ prisma.config.ts + ├─ prisma.config.js └─ src ├─ app.js ├─ config + │ └─ prisma.js ├─ controllers + │ └─ auth.controller.js ├─ middleware - ├─ models ├─ server.js ├─ services - └─ utils -``` + │ └─ auth.service.js + ├─ utils + └─ validators + └─ auth.validator.js + +``` \ No newline at end of file diff --git a/client/.gitignore b/client/.gitignore index a547bf3..d823d0d 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -22,3 +22,6 @@ dist-ssr *.njsproj *.sln *.sw? + + +.env \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 93eacf3..a0c3206 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,11 +8,15 @@ "name": "client", "version": "0.0.0", "dependencies": { + "@marsidev/react-turnstile": "^1.5.3", + "@react-oauth/google": "^0.13.5", "@tailwindcss/vite": "^4.3.2", + "axios": "^1.18.1", "lucide-react": "^1.23.0", "react": "^19.2.7", "react-dom": "^19.2.7", "react-router-dom": "^7.18.1", + "recharts": "^3.10.1", "tailwindcss": "^4.3.2" }, "devDependencies": { @@ -537,6 +541,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@marsidev/react-turnstile": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@marsidev/react-turnstile/-/react-turnstile-1.5.3.tgz", + "integrity": "sha512-8Dij2jiNGNczq1U4EKpO4do2XepcTPxSMc2ZzvHndO+gcp68tvMULm27z2P99rGkdB89hc3452NZeu2Rti4g6A==", + "license": "MIT", + "peerDependencies": { + "react": "^17.0.2 || ^18.0.0 || ^19.0", + "react-dom": "^17.0.2 || ^18.0.0 || ^19.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -564,6 +578,42 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@react-oauth/google": { + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.13.5.tgz", + "integrity": "sha512-xQWri2s/3nNekZJ4uuov2aAfQYu83bN3864KcFqw2pK1nNbFurQIjPFDXhWaKH3IjYJ2r/9yyIIpsn5lMqrheQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.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/@rolldown/binding-android-arm64": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", @@ -812,6 +862,18 @@ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "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/@tailwindcss/node": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", @@ -1079,6 +1141,69 @@ "tslib": "^2.4.0" } }, + "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.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "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/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -1104,7 +1229,7 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1120,6 +1245,12 @@ "@types/react": "^19.2.0" } }, + "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/@vitejs/plugin-react": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", @@ -1169,6 +1300,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -1186,6 +1329,24 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1256,6 +1417,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001799", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", @@ -1277,6 +1451,27 @@ ], "license": "CC-BY-4.0" }, + "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/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1316,14 +1511,134 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "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.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "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/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1337,6 +1652,12 @@ } } }, + "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", @@ -1344,6 +1665,15 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1353,6 +1683,20 @@ "node": ">=8" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.381", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.381.tgz", @@ -1373,6 +1717,61 @@ "node": ">=10.13.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", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-toolkit": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.49.0.tgz", + "integrity": "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1581,6 +1980,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "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", @@ -1670,6 +2075,42 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1684,6 +2125,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -1694,6 +2144,43 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1720,12 +2207,63 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -1743,6 +2281,19 @@ "hermes-estree": "0.25.1" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -1753,6 +2304,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "11.1.15", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.15.tgz", + "integrity": "sha512-VrNANlmnWQnh5COXIIOQXM9oOJw7naGKlBT74ZOOR6lpVXc3gFEu9FJLDFcpCJ2j+NWr8TIwtWD//T6ZX6TKiQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -1763,6 +2324,15 @@ "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-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2173,6 +2743,36 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -2193,7 +2793,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -2357,6 +2956,15 @@ "node": ">= 0.8.0" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2388,6 +2996,36 @@ "react": "^19.2.7" } }, + "node_modules/react-is": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "7.18.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", @@ -2426,6 +3064,57 @@ "react-dom": ">=18" } }, + "node_modules/recharts": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz", + "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^11.1.8", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, "node_modules/rolldown": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", @@ -2532,6 +3221,12 @@ "url": "https://opencollective.com/webpack" } }, + "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/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -2609,6 +3304,37 @@ "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/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": "8.1.0", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz", diff --git a/client/package.json b/client/package.json index 7569686..aec263f 100644 --- a/client/package.json +++ b/client/package.json @@ -10,11 +10,15 @@ "preview": "vite preview" }, "dependencies": { + "@marsidev/react-turnstile": "^1.5.3", + "@react-oauth/google": "^0.13.5", "@tailwindcss/vite": "^4.3.2", + "axios": "^1.18.1", "lucide-react": "^1.23.0", "react": "^19.2.7", "react-dom": "^19.2.7", "react-router-dom": "^7.18.1", + "recharts": "^3.10.1", "tailwindcss": "^4.3.2" }, "devDependencies": { diff --git a/client/src/App.jsx b/client/src/App.jsx index f081195..49bea84 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,19 +1,194 @@ -import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; -// import Login from "./pages/Login"; +import { + BrowserRouter, + Navigate, + Route, + Routes, + useLocation, +} from "react-router-dom"; +import Login from "./pages/login"; import Signup from "./pages/signup"; +import Forgotpassword from "./pages/forgotpassword"; +import WorkspaceDashboard from "./pages/WorkspaceDashboard"; +import CompleteProfile from "./pages/completeProfile"; +import ProtectedRoute from "./components/common/ProtectedRoute"; +import ProtectedAdminRoute from "./components/ProtectedAdminRoute"; +import AdminDashboard from "./pages/AdminDashboard"; +import OrganizationDashboard from "./pages/OrganizationDashboard"; +import OrganizationMembers from "./pages/OrganizationMembers"; +import Workspaces from "./pages/Workspaces"; +import CreateWorkspaceWizard from "./pages/CreateWorkspaceWizard"; +import WorkspaceDetails from "./pages/WorkspaceDetails"; +import PageLoader from "./components/common/PageLoader"; +import { + NavigationLoadingProvider, + useNavigationLoading, +} from "./context/NavigationLoadingContext"; import "./index.css"; +import GithubCallback from "./pages/githubCallback"; +import Projects from "./pages/Projects"; +import Sprints from "./pages/Sprints"; +import TasksKanban from "./pages/TasksKanban"; + +function AppRoutes() { + const location = useLocation(); + const { goTo } = useNavigationLoading(); + + const handleLinkClick = (event) => { + const link = event.target.closest("a"); + + if ( + !link || + event.defaultPrevented || + event.button !== 0 || + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey || + link.target === "_blank" || + link.hasAttribute("download") + ) { + return; + } + + const url = new URL(link.href, window.location.origin); + + // Do not intercept external links, such as normal GitHub links. + if (url.origin !== window.location.origin) { + return; + } + + const destination = `${url.pathname}${url.search}${url.hash}`; + const currentPage = `${location.pathname}${location.search}${location.hash}`; + + // Do not show a loader for a link to the page already open. + if (destination === currentPage) { + return; + } + + event.preventDefault(); + + goTo(destination); + }; -function App() { return ( - +
+ + - {/* } /> */} + } /> } /> - {/* Redirect root to login for now */} - } /> + } /> + } /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + {/* Organization Routes Protected by Auth */} + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + }> + } /> + + + } /> +
+ ); +} + +function App() { + return ( + + + + ); } -export default App; +export default App; \ No newline at end of file diff --git a/client/src/assets/eyeOff.svg b/client/src/assets/eyeOff.svg new file mode 100644 index 0000000..5a0b98a --- /dev/null +++ b/client/src/assets/eyeOff.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/client/src/assets/eyeOn.svg b/client/src/assets/eyeOn.svg new file mode 100644 index 0000000..4d27ebc --- /dev/null +++ b/client/src/assets/eyeOn.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/client/src/assets/github.svg b/client/src/assets/github.svg new file mode 100644 index 0000000..84e4d2f --- /dev/null +++ b/client/src/assets/github.svg @@ -0,0 +1,19 @@ + + + + + github [#142] + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/src/assets/google.svg b/client/src/assets/google.svg new file mode 100644 index 0000000..3ffa2aa --- /dev/null +++ b/client/src/assets/google.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/client/src/components/ProtectedAdminRoute.jsx b/client/src/components/ProtectedAdminRoute.jsx new file mode 100644 index 0000000..22bd8ea --- /dev/null +++ b/client/src/components/ProtectedAdminRoute.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Navigate, Outlet } from 'react-router-dom'; + +const ProtectedAdminRoute = () => { + const userStr = localStorage.getItem("user"); + + if (!userStr) { + return ; + } + + const user = JSON.parse(userStr); + + if (user.platformRole !== 'PLATFORM_ADMIN') { + return ; + } + + return ; +}; + +export default ProtectedAdminRoute; diff --git a/client/src/components/common/Card.jsx b/client/src/components/common/Card.jsx new file mode 100644 index 0000000..0d544ee --- /dev/null +++ b/client/src/components/common/Card.jsx @@ -0,0 +1,13 @@ +import React from "react"; + +const Card = ({ children, className = "" }) => { + return ( +
+ {children} +
+ ); +}; + +export default Card; diff --git a/client/src/components/common/PageLoader.jsx b/client/src/components/common/PageLoader.jsx new file mode 100644 index 0000000..d6150f1 --- /dev/null +++ b/client/src/components/common/PageLoader.jsx @@ -0,0 +1,23 @@ +// client/src/components/common/PageLoader.jsx + +import { LoaderCircle } from "lucide-react"; +import { useNavigationLoading } from "../../context/NavigationLoadingContext.jsx"; + +function PageLoader() { + const { isNavigating } = useNavigationLoading(); + + if (!isNavigating) { + return null; + } + + return ( +
+
+ + Loading... +
+
+ ); +} + +export default PageLoader; \ No newline at end of file diff --git a/client/src/components/common/ProtectedRoute.jsx b/client/src/components/common/ProtectedRoute.jsx new file mode 100644 index 0000000..7e69458 --- /dev/null +++ b/client/src/components/common/ProtectedRoute.jsx @@ -0,0 +1,13 @@ +import {Navigate} from "react-router-dom"; + +function ProtectedRoute({children}){ + const user = localStorage.getItem("user"); + + if(!user){ + return ; + } + + return children; +} + +export default ProtectedRoute; \ No newline at end of file diff --git a/client/src/components/layout/KPIs/StatsCard.jsx b/client/src/components/layout/KPIs/StatsCard.jsx new file mode 100644 index 0000000..6fe5aae --- /dev/null +++ b/client/src/components/layout/KPIs/StatsCard.jsx @@ -0,0 +1,19 @@ +import React from "react"; + +const StatsCard = ({ title, value, subtitle, icon: Icon, borderColor, bgColor, iconColor, hoverBorderColor, bgIcon}) => { + return ( +
+
+ {title} +
+ +
+
+

{value}

+

{subtitle}

+
+ ); +}; + +export default StatsCard; \ No newline at end of file diff --git a/client/src/components/layout/KPIs/StatsData.js b/client/src/components/layout/KPIs/StatsData.js new file mode 100644 index 0000000..cd9c9ba --- /dev/null +++ b/client/src/components/layout/KPIs/StatsData.js @@ -0,0 +1,73 @@ +import React from "react"; +import { Folder, GitPullRequestArrow, Code, CircleAlert, CloudUpload, CloudUploadIcon, CheckCircle, CheckCircle2Icon} from "lucide-react"; + +const StatsData = [ + { + title: "Active Projects", + value: 6, + subtitle: "2 from last week", + icon: Folder, + borderColor: "border-violet-500/30", + bgColor: "bg-gradient-to-br from-violet-950/60 to-slate-900", + iconColor: "text-violet-400", + bgIcon: "bg-violet-500/20", + hoverBorderColor: "hover:border-violet-400", + }, + { + title: "Pull Requests", + value: 9, + subtitle: "4 Awaiting Review", + icon: GitPullRequestArrow, + borderColor: "border-sky-500/30", + bgColor: "bg-gradient-to-br from-cyan-950/60 to-slate-900", + iconColor: "text-sky-400", + bgIcon: "bg-sky-500/20", + hoverBorderColor: "hover:border-sky-400", + }, + { + title: "Commits", + value: 156, + subtitle: "18% from last week", + icon: Code, + borderColor: "border-blue-500/30", + bgColor: "bg-gradient-to-br from-blue-950/60 to-slate-900", + iconColor: "text-blue-400", + bgIcon: "bg-blue-500/20", + hoverBorderColor: "hover:border-blue-400", + }, + { + title: "Open Issues", + value: 18, + subtitle: "2 Critical", + icon: CircleAlert, + borderColor: "border-green-500/30", + bgColor: "bg-gradient-to-br from-green-950/60 to-slate-900", + iconColor: "text-green-400", + bgIcon: "bg-green-500/20", + hoverBorderColor: "hover:border-green-400", + }, + { + title: "Deployments", + value: 4, + subtitle: "1 from last week", + icon: CloudUploadIcon, + borderColor: "border-orange-500/30", + bgColor: "bg-gradient-to-br from-orange-950/60 to-slate-900", + iconColor: "text-orange-400", + bgIcon: "bg-orange-500/20", + hoverBorderColor: "hover:border-orange-400", + }, + { + title: "Tasks Done", + value: 32, + subtitle: "68% Completed", + icon: CheckCircle2Icon, + borderColor: "border-purple-500/30", + bgColor: "bg-gradient-to-br from-purple-950/60 to-slate-900", + iconColor: "text-purple-400", + bgIcon: "bg-purple-500/20", + hoverBorderColor: "hover:border-purple-400", + }, +]; + +export default StatsData; diff --git a/client/src/components/layout/KPIs/StatsGrid.jsx b/client/src/components/layout/KPIs/StatsGrid.jsx new file mode 100644 index 0000000..e8f5a68 --- /dev/null +++ b/client/src/components/layout/KPIs/StatsGrid.jsx @@ -0,0 +1,102 @@ +import React from "react"; +import StatsCard from "./StatsCard.jsx"; +import { Folder, Play, CheckCircle2Icon, CircleAlert, CheckCircle, Code } from "lucide-react"; + +const getStatsData = (kpis) => { + return [ + { + title: "Active Projects", + value: kpis?.totalProjects || 0, + subtitle: "In this workspace", + icon: Folder, + borderColor: "border-violet-500/30", + bgColor: "bg-gradient-to-br from-violet-950/60 to-slate-900", + iconColor: "text-violet-400", + bgIcon: "bg-violet-500/20", + hoverBorderColor: "hover:border-violet-400", + }, + { + title: "Active Sprints", + value: kpis?.activeSprints || 0, + subtitle: "Currently running", + icon: Play, + borderColor: "border-sky-500/30", + bgColor: "bg-gradient-to-br from-cyan-950/60 to-slate-900", + iconColor: "text-sky-400", + bgIcon: "bg-sky-500/20", + hoverBorderColor: "hover:border-sky-400", + }, + { + title: "Tasks Completed", + value: kpis?.tasksCompleted || 0, + subtitle: "Across all projects", + icon: CheckCircle2Icon, + borderColor: "border-blue-500/30", + bgColor: "bg-gradient-to-br from-blue-950/60 to-slate-900", + iconColor: "text-blue-400", + bgIcon: "bg-blue-500/20", + hoverBorderColor: "hover:border-blue-400", + }, + { + title: "Open Bugs", + value: kpis?.openBugs || 0, + subtitle: "Requires attention", + icon: CircleAlert, + borderColor: "border-red-500/30", + bgColor: "bg-gradient-to-br from-red-950/60 to-slate-900", + iconColor: "text-red-400", + bgIcon: "bg-red-500/20", + hoverBorderColor: "hover:border-red-400", + }, + { + title: "Total Sprints", + value: kpis?.totalSprints || 0, + subtitle: "Historical data", + icon: Code, + borderColor: "border-orange-500/30", + bgColor: "bg-gradient-to-br from-orange-950/60 to-slate-900", + iconColor: "text-orange-400", + bgIcon: "bg-orange-500/20", + hoverBorderColor: "hover:border-orange-400", + }, + { + title: "Total Tasks", + value: kpis?.totalTasks || 0, + subtitle: "Total recorded", + icon: CheckCircle, + borderColor: "border-purple-500/30", + bgColor: "bg-gradient-to-br from-purple-950/60 to-slate-900", + iconColor: "text-purple-400", + bgIcon: "bg-purple-500/20", + hoverBorderColor: "hover:border-purple-400", + }, + ]; +}; + +const StatsGrid = ({ kpis }) => { + const stats = getStatsData(kpis); + + return ( +
+
+ {stats.map((stat) => ( + + ))} +
+
+ ); +}; + +export default StatsGrid; \ No newline at end of file diff --git a/client/src/components/layout/LowerMiddleSection/LowerMiddleSection.jsx b/client/src/components/layout/LowerMiddleSection/LowerMiddleSection.jsx new file mode 100644 index 0000000..9a4cd98 --- /dev/null +++ b/client/src/components/layout/LowerMiddleSection/LowerMiddleSection.jsx @@ -0,0 +1,20 @@ +import React from 'react' +import { RecentActivity } from './RecentActivity'; +import { PullRequests } from './PullRequests'; +import { OpenIssues } from './OpenIssues'; + +const LowerMiddleSection = ({ recentActivity, orgId, workspaceId }) => { + return ( +
+
+ + +
+ +
+
+
+ ); +} + +export default LowerMiddleSection; \ No newline at end of file diff --git a/client/src/components/layout/LowerMiddleSection/OpenIssues.jsx b/client/src/components/layout/LowerMiddleSection/OpenIssues.jsx new file mode 100644 index 0000000..374e0d3 --- /dev/null +++ b/client/src/components/layout/LowerMiddleSection/OpenIssues.jsx @@ -0,0 +1,135 @@ +import { useEffect, useState } from "react"; +import { CircleAlert, LoaderCircle } from "lucide-react"; +import Card from "../../common/Card"; +import axios from "axios"; + +const getPriorityStyles = (priority) => { + switch (priority) { + case "Critical": + return "bg-red-500/20 text-red-400"; + + case "High": + return "bg-orange-500/20 text-orange-400"; + + case "Medium": + return "bg-yellow-500/20 text-yellow-400"; + + case "Low": + return "bg-blue-500/20 text-blue-400"; + + default: + return "bg-slate-500/20 text-slate-400"; + } +}; + +const getIconStyles = (priority) => { + switch (priority) { + case "Critical": + return "text-red-400"; + + case "High": + return "text-orange-400"; + + case "Medium": + return "text-yellow-400"; + + case "Low": + return "text-blue-400"; + + default: + return "text-slate-400"; + } +}; + +export const OpenIssues = ({ orgId, workspaceId }) => { + const [issues, setIssues] = useState([]); + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const loadIssues = async () => { + try { + const result = await axios.get(`http://localhost:5000/api/organizations/${orgId}/workspaces/${workspaceId}/github/issues`, { withCredentials: true }); + setIssues(result.data.data); + } catch (err) { + setError( + err.response?.data?.message || "Could not load open issues." + ); + } finally { + setIsLoading(false); + } + }; + + loadIssues(); + }, []); + + return ( + +
+

Open Issues

+
+ + {isLoading && ( +
+ +
+ )} + + {!isLoading && error && ( +

{error}

+ )} + + {!isLoading && !error && issues.length === 0 && ( +

+ No open GitHub issues. +

+ )} + + {!isLoading && !error && issues.length > 0 && ( + + )} +
+ ); +}; + +export default OpenIssues; \ No newline at end of file diff --git a/client/src/components/layout/LowerMiddleSection/PullRequests.jsx b/client/src/components/layout/LowerMiddleSection/PullRequests.jsx new file mode 100644 index 0000000..9acd024 --- /dev/null +++ b/client/src/components/layout/LowerMiddleSection/PullRequests.jsx @@ -0,0 +1,126 @@ +import { useEffect, useState } from "react"; +import { + GitPullRequest, + LoaderCircle, + MessageSquare, +} from "lucide-react"; +import Card from "../../common/Card"; +import axios from "axios"; + +const getStatusStyles = (status) => { + switch (status) { + case "Draft": + return "bg-slate-500/20 text-slate-300"; + + case "Review": + return "bg-purple-500/20 text-purple-400"; + + case "Open": + return "bg-cyan-500/20 text-cyan-400"; + + default: + return "bg-slate-500/20 text-slate-400"; + } +}; + +export const PullRequests = ({ orgId, workspaceId }) => { + const [pullRequests, setPullRequests] = useState([]); + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const loadPullRequests = async () => { + try { + const result = await axios.get(`http://localhost:5000/api/organizations/${orgId}/workspaces/${workspaceId}/github/pull-requests`, { withCredentials: true }); + setPullRequests(result.data.data); + } catch (err) { + setError( + err.response?.data?.message || + "Could not load pull requests." + ); + } finally { + setIsLoading(false); + } + }; + + loadPullRequests(); + }, []); + + return ( + +
+

+ Pull Requests +

+
+ + {isLoading && ( +
+ +
+ )} + + {!isLoading && error && ( +

{error}

+ )} + + {!isLoading && !error && pullRequests.length === 0 && ( +

+ No open GitHub pull requests. +

+ )} + + {!isLoading && !error && pullRequests.length > 0 && ( + + )} +
+ ); +}; + +export default PullRequests; \ No newline at end of file diff --git a/client/src/components/layout/LowerMiddleSection/RecentActivity.jsx b/client/src/components/layout/LowerMiddleSection/RecentActivity.jsx new file mode 100644 index 0000000..c27eaaa --- /dev/null +++ b/client/src/components/layout/LowerMiddleSection/RecentActivity.jsx @@ -0,0 +1,82 @@ +import React from "react"; +import Card from "../../common/Card"; +import { MessageSquare, GitMerge, CircleDot, GitCommit, CheckCircle, PlusCircle, Play, CheckSquare } from "lucide-react"; + +const timeAgo = (dateStr) => { + const diff = (new Date() - new Date(dateStr)) / 1000; + if (diff < 60) return "just now"; + if (diff < 3600) return Math.floor(diff / 60) + "m ago"; + if (diff < 86400) return Math.floor(diff / 3600) + "h ago"; + return Math.floor(diff / 86400) + "d ago"; +}; + +const getActivityConfig = (type) => { + switch(type) { + case 'PROJECT_CREATED': + return { icon: PlusCircle, bg: 'bg-emerald-500/20', color: 'text-emerald-400' }; + case 'TASK_CREATED': + return { icon: CircleDot, bg: 'bg-blue-500/20', color: 'text-blue-400' }; + case 'TASK_UPDATED': + return { icon: MessageSquare, bg: 'bg-orange-500/20', color: 'text-orange-400' }; + case 'TASK_COMPLETED': + return { icon: CheckSquare, bg: 'bg-emerald-500/20', color: 'text-emerald-400' }; + case 'SPRINT_CREATED': + return { icon: PlusCircle, bg: 'bg-purple-500/20', color: 'text-purple-400' }; + case 'SPRINT_STARTED': + return { icon: Play, bg: 'bg-cyan-500/20', color: 'text-cyan-400' }; + case 'SPRINT_COMPLETED': + return { icon: CheckCircle, bg: 'bg-green-500/20', color: 'text-green-400' }; + default: + return { icon: CircleDot, bg: 'bg-slate-500/20', color: 'text-slate-400' }; + } +}; + +export const RecentActivity = ({ activities }) => { + return ( + +
+

+ Recent Activity +

+
+ +
+ {!activities || activities.length === 0 ? ( +

No recent activity.

+ ) : ( + activities.map((activity) => { + const config = getActivityConfig(activity.type); + const Icon = config.icon; + + return ( +
+
+ +
+ {activity.user?.avatar ? ( + Avatar + ) : ( +
+ {activity.user?.username?.charAt(0) || '?'} +
+ )} +
+

+ {activity.user?.username}{" "} + {activity.message.replace(`User ${activity.user?.username} `, "")} +

+

{timeAgo(activity.createdAt)}

+
+
+ ); + }) + )} +
+
+ ); +}; + +export default RecentActivity; diff --git a/client/src/components/layout/MiddleSection/CICDStatus.jsx b/client/src/components/layout/MiddleSection/CICDStatus.jsx new file mode 100644 index 0000000..e25c6e6 --- /dev/null +++ b/client/src/components/layout/MiddleSection/CICDStatus.jsx @@ -0,0 +1,70 @@ +import React from "react"; +import Card from "../../common/Card"; +import { ArrowRight } from "lucide-react"; + +const pipelineSteps = [ + { + name: "Build", + status: "Passed", + time: "2 min ago", + icon: "✅", + color: "text-green-400", + }, + { + name: "Tests", + status: "143 Passed", + time: "2 min ago", + icon: "✅", + color: "text-green-400", + }, + { + name: "Lint", + status: "Passed", + time: "2 min ago", + icon: "✅", + color: "text-green-400", + }, + { + name: "Deployment", + status: "Deployed", + time: "5 min ago", + icon: "🚀", + color: "text-orange-400", + }, +]; + +export const CICDStatus = () => { + return ( + +

CI/CD Status

+ +
+ {pipelineSteps.map((step) => ( +
+
{step.name}
+ +
+ {step.icon} + {step.status} +
+ +
{step.time}
+
+ ))} +
+ +
+ +
+
+ ); +}; + +export default CICDStatus; diff --git a/client/src/components/layout/MiddleSection/CurrentSprint.jsx b/client/src/components/layout/MiddleSection/CurrentSprint.jsx new file mode 100644 index 0000000..d188850 --- /dev/null +++ b/client/src/components/layout/MiddleSection/CurrentSprint.jsx @@ -0,0 +1,86 @@ +import { LoaderCircle } from "lucide-react"; +import Card from "../../common/Card"; + +const StatItem = ({ label, value }) => { + return ( +
+

{label}

+

+ {value} +

+
+ ); +}; + +const formatDate = (date) => { + if (!date) return "No due date"; + return new Intl.DateTimeFormat("en-IN", { + day: "numeric", + month: "short", + year: "numeric", + }).format(new Date(date)); +}; + +export const CurrentSprint = ({ sprint }) => { + if (!sprint) { + return ( + +

+ Current Sprint +

+

No active sprint. Start a sprint from the Sprints page.

+
+ ); + } + + // Calculate days left + const daysLeft = sprint.endDate + ? Math.max(0, Math.ceil((new Date(sprint.endDate) - new Date()) / (1000 * 60 * 60 * 24))) + : null; + + return ( + +
+

+ Current Sprint +

+
+ {sprint.name} +
+
+ +
+

+ {sprint.goal || "No goal specified"} +

+

+ Due {formatDate(sprint.endDate)} + {daysLeft !== null && ` (${daysLeft} days left)`} +

+
+ +
+
+
+
+ + {sprint.progress}% + +
+ +
+
+ + + + +
+
+ + ); +}; + +export default CurrentSprint; \ No newline at end of file diff --git a/client/src/components/layout/MiddleSection/MiddleSection.jsx b/client/src/components/layout/MiddleSection/MiddleSection.jsx new file mode 100644 index 0000000..79c05b2 --- /dev/null +++ b/client/src/components/layout/MiddleSection/MiddleSection.jsx @@ -0,0 +1,20 @@ +import React from 'react' +import { CurrentSprint } from './CurrentSprint'; +import { ProjectOverview } from './ProjectOverview'; +import { CICDStatus } from './CICDStatus'; + +const MiddleSection = ({ currentSprint, projectOverview }) => { + return ( +
+
+ + +
+ +
+
+
+ ); +} + +export default MiddleSection; \ No newline at end of file diff --git a/client/src/components/layout/MiddleSection/ProjectOverview.jsx b/client/src/components/layout/MiddleSection/ProjectOverview.jsx new file mode 100644 index 0000000..efac7b6 --- /dev/null +++ b/client/src/components/layout/MiddleSection/ProjectOverview.jsx @@ -0,0 +1,58 @@ +import React from "react"; +import Card from "../../common/Card"; +import { Link, useParams } from "react-router-dom"; + +export const ProjectOverview = ({ projects }) => { + const { orgId, workspaceId } = useParams(); + const activeProject = projects && projects.length > 0 ? projects[0] : null; + + if (!activeProject) { + return ( + +

Project Overview

+

No active projects yet.

+ + Create Project + +
+ ); + } + + return ( + +
+

+ Project Overview +

+ View All +
+
+
+ {activeProject.name.charAt(0)} +
+
+

{activeProject.name}

+

{activeProject.description || "No description provided."}

+
+
+
+

+ Overall Progress + {activeProject.progress}% +

+ +
+
+
+
+
+
+ + ); +}; diff --git a/client/src/components/layout/Navbar.jsx b/client/src/components/layout/Navbar.jsx new file mode 100644 index 0000000..4e083d9 --- /dev/null +++ b/client/src/components/layout/Navbar.jsx @@ -0,0 +1,168 @@ +import { Search, Bell, Menu, MessageCircle, LogOut } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { useNavigationLoading } from "../../context/NavigationLoadingContext.jsx"; +import githubIcon from "/src/assets/github.svg"; +import { logout } from "../../services/googleAuthService.js"; + +// ADDED githubData PROP +const Navbar = ({ toggleSidebar, githubData }) => { + const { goTo } = useNavigationLoading(); + const dropdownRef = useRef(null); + + const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false); + const [imageFailed, setImageFailed] = useState(false); + + const storedUser = localStorage.getItem("user"); + const user = storedUser ? JSON.parse(storedUser) : null; + + const username = user?.username || "User"; + const avatar = user?.avatar; + + const userInitial = username.charAt(0).toUpperCase(); + + const handleLogout = async () => { + try { + await logout(); + } catch (error) { + console.error("Logout request failed:", error); + } finally { + // Always remove frontend login data. + localStorage.removeItem("user"); + + // Prevents Back from returning to dashboard. + goTo("/login", { replace: true }); + } + }; + + useEffect(() => { + const closeDropdown = (event) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target) + ) { + setIsProfileMenuOpen(false); + } + }; + + document.addEventListener("mousedown", closeDropdown); + + return () => { + document.removeEventListener("mousedown", closeDropdown); + }; + }, []); + + return ( +
+
+ + +
+

+ Dashboard +

+ +

+ Welcome back, {username}! Here's what's happening with your + projects. +

+
+
+ +
+ + + + + +
+ +
+
+ + + +
+ + + + {/* MODIFIED: GitHub Icon now has an onClick handler and only shows if data is missing */} + {!githubData && ( +
window.location.href = "http://localhost:5000/api/auth/github?action=connect"} + className="flex h-5 w-5 cursor-pointer items-center justify-center font-medium transition-transform duration-300 hover:scale-110" + title="Connect your GitHub account" + > + GitHub +
+ )} + +
+ + + {isProfileMenuOpen && ( +
+
+

+ {username} +

+ +

+ {user?.email} +

+
+ + +
+ )} +
+
+
+ ); +}; + +export default Navbar; \ No newline at end of file diff --git a/client/src/components/layout/RepositoryHealthSection/RepositoryHealth.jsx b/client/src/components/layout/RepositoryHealthSection/RepositoryHealth.jsx new file mode 100644 index 0000000..5fa38ee --- /dev/null +++ b/client/src/components/layout/RepositoryHealthSection/RepositoryHealth.jsx @@ -0,0 +1,111 @@ +import React from "react"; +import { ResponsiveContainer, LineChart, Line } from "recharts"; + +const repositoryHealthData = [ + { + title: "Code Coverage", + value: "92%", + type: "progress", + progress: 92, + color: "bg-green-500", + }, + { + title: "Test Passing Rate", + value: "98%", + type: "progress", + progress: 98, + color: "bg-green-500", + }, + { + title: "Open Issues", + value: "18", + type: "chart", + chartColor: "#ef4444", + }, + { + title: "Technical Debt", + value: "4.2 hrs", + type: "chart", + chartColor: "#f97316", + }, + { + title: "Code Smells", + value: "12", + type: "chart", + chartColor: "#a855f7", + }, + { + title: "Duplications", + value: "3.1%", + type: "chart", + chartColor: "#3b82f6", + }, +]; + +const chartData = [ + { value: 20 }, + { value: 35 }, + { value: 28 }, + { value: 45 }, + { value: 30 }, + { value: 55 }, + { value: 48 }, +]; + +const HealthMetric = ({ title, value, type, progress, color, chartColor }) => { + return ( +
+ {/* Title */} +

{title}

+ + {/* Value */} +

{value}

+ + {/* Progress Bars */} + {type === "progress" ? ( +
+
+
+ ) : ( +
+ + + + + +
+ )} +
+ ); +}; + +const RepositoryHealth = ({ orgId, workspaceId }) => { + return ( +
+ {/* Header */} +
+

+ Repository Health +

+
+ + {/* Metrics Grid */} +
+ {repositoryHealthData.map((item) => ( + + ))} +
+
+ ); +}; + +export default RepositoryHealth; diff --git a/client/src/components/layout/RightSideSection/RightSideBar.jsx b/client/src/components/layout/RightSideSection/RightSideBar.jsx new file mode 100644 index 0000000..c0ddc95 --- /dev/null +++ b/client/src/components/layout/RightSideSection/RightSideBar.jsx @@ -0,0 +1,18 @@ +import React from 'react' +import TeamMember from './TeamMember'; +import UpcomingEvents from './UpcomingEvents'; +import TeamChat from './TeamChat'; + +export const RightSideBar = () => { + return ( +
+
+ + + +
+
+ ); +} + +export default RightSideBar; \ No newline at end of file diff --git a/client/src/components/layout/RightSideSection/TeamChat.jsx b/client/src/components/layout/RightSideSection/TeamChat.jsx new file mode 100644 index 0000000..5276aab --- /dev/null +++ b/client/src/components/layout/RightSideSection/TeamChat.jsx @@ -0,0 +1,66 @@ +import React from "react"; + +const recentChats = [ + { + id: 1, + name: "Soham A.", + time: "10:30 AM", + message: "OAuth flow is ready for review.", + avatar: "https://i.pravatar.cc/150?u=4", + }, + { + id: 2, + name: "Ankur Y.", + time: "10:28 AM", + message: "Pushed the latest backend changes.", + avatar: "https://i.pravatar.cc/150?u=3", + }, + { + id: 3, + name: "Sumit S.", + time: "10:25 AM", + message: "Updated the UI for the dashboard.", + avatar: "https://i.pravatar.cc/150?u=2", + }, +]; + +export default function TeamChat() { + return ( +
+
+

+ Team Chat (Recent) +

+ + View all + +
+ +
+ {recentChats.map((chat) => ( +
+ {chat.name} +
+
+

+ {chat.name} +

+

+ {chat.time} +

+
+

{chat.message}

+
+
+ ))} +
+
+ ); +} diff --git a/client/src/components/layout/RightSideSection/TeamMember.jsx b/client/src/components/layout/RightSideSection/TeamMember.jsx new file mode 100644 index 0000000..a9ffb7b --- /dev/null +++ b/client/src/components/layout/RightSideSection/TeamMember.jsx @@ -0,0 +1,108 @@ +import React from "react"; + +export const teamMembers = [ + { + id: 1, + name: "Ankur Y. (You)", + role: "Admin", + status: "Online", + statusColor: "bg-emerald-500", + avatar: "https://i.pravatar.cc/150?u=1", + }, + { + id: 2, + name: "Sarah J.", + role: "Frontend Developer", + status: "Online", + statusColor: "bg-emerald-500", + avatar: "https://i.pravatar.cc/150?u=2", + }, + { + id: 3, + name: "Soham A.", + role: "Backend Developer", + status: "Online", + statusColor: "bg-emerald-500", + avatar: "https://i.pravatar.cc/150?u=3", + }, + { + id: 4, + name: "Anas S.", + role: "DevOps Engineer", + status: "Away", + statusColor: "bg-yellow-500", + avatar: "https://i.pravatar.cc/150?u=4", + }, + { + id: 5, + name: "Sumit S.", + role: "QA Engineer", + status: "Offline", + statusColor: "bg-gray-500", + avatar: "https://i.pravatar.cc/150?u=5", + }, + { + id: 6, + name: "Maria K.", + role: "UI/UX Designer", + status: "Online", + statusColor: "bg-emerald-500", + avatar: "https://i.pravatar.cc/150?u=6", + }, +]; + +export default function TeamMember() { + return ( +
+
+

Team Members

+ + View all + +
+ +
+ {teamMembers.map((member) => ( +
+
+ {member.name} +
+

+ {member.name} +

+

{member.jobTitle}

+
+
+ +
+ + + {member.status} + +
+
+ ))} +
+
+ ); +} diff --git a/client/src/components/layout/RightSideSection/UpcomingEvents.jsx b/client/src/components/layout/RightSideSection/UpcomingEvents.jsx new file mode 100644 index 0000000..de364e7 --- /dev/null +++ b/client/src/components/layout/RightSideSection/UpcomingEvents.jsx @@ -0,0 +1,63 @@ +import React from "react"; +import { Calendar } from "lucide-react"; + +const upcomingEvents = [ + { + id: 1, + title: "Sprint Planning", + date: "May 14, 2025 • 10:00 AM", + color: "bg-purple-500/20", + iconColor: "text-purple-500", + }, + { + id: 2, + title: "Code Review Sync", + date: "May 15, 2025 • 04:00 PM", + color: "bg-indigo-500/20", + iconColor: "text-indigo-500", + }, + { + id: 3, + title: "Demo Day", + date: "May 16, 2025 • 11:00 AM", + color: "bg-blue-500/20", + iconColor: "text-blue-500", + }, +]; + +export default function UpcomingEvents() { + return ( +
+
+

Upcoming Events

+ + View calendar + +
+ +
+ {upcomingEvents.map((event) => ( +
+
+ +
+
+

+ {event.title} +

+

{event.date}

+
+
+ ))} +
+
+ ); +} diff --git a/client/src/components/layout/Sidebar.jsx b/client/src/components/layout/Sidebar.jsx new file mode 100644 index 0000000..65eab1d --- /dev/null +++ b/client/src/components/layout/Sidebar.jsx @@ -0,0 +1,222 @@ +import { Book, BookAIcon, BookCheck, BookCopy, BookIcon, BookOpenText, BubblesIcon, Calendar, Calendar1Icon, ChartBar, CircleArrowDown, CircleCheck, Folder, GitBranchMinusIcon, GitCommit, GitCommitHorizontalIcon, GitCompare, GitMergeConflict, GitPullRequest, GitPullRequestArrow, HomeIcon, LucideGitCommit, LucideMirrorRectangular, MessageCircle, Settings, Ticket, User2, User2Icon, UserCircleIcon, UserIcon, UserRound, PanelLeftOpen, PanelLeftClose, LayoutDashboard, CircleUserRound, ShieldCheck} from "lucide-react"; + +import { useOrganization } from "../../context/OrganizationContext"; +import {useState} from 'react' +import { useParams, useNavigate } from 'react-router-dom'; + +function Sidebar({ isOpen, setIsOpen }) { + const [isCollapsed, setIsCollapsed] = useState(false); + const { orgId, workspaceId } = useParams(); + const navigate = useNavigate(); + const { organizations, currentOrg, switchOrganization, loading } = useOrganization(); + + const userStr = localStorage.getItem("user"); + const user = userStr ? JSON.parse(userStr) : null; + + return ( + <> + {isOpen && ( +
setIsOpen(false)} + /> + )} +
+
+ {!isCollapsed && Devhub} + +
+ {/* Organization Switcher */} + {!loading && organizations.length > 0 && ( +
+ +
+ )} + {workspaceId && ( +
+
navigate(`/organizations/${orgId}/workspaces/${workspaceId}/dashboard`)} + className="flex items-center h-10 px-3 gap-2 hover:bg-[#1c1f2e] hover:text-white rounded-lg font-medium cursor-pointer transition-colors" + > + + + Dashboard + +
+
navigate(`/organizations/${orgId}/workspaces/${workspaceId}/projects`)} + className="flex items-center h-10 px-3 gap-2 hover:bg-[#1c1f2e] hover:text-white rounded-lg cursor-pointer transition-colors" + > + + + Projects + +
+
navigate(`/organizations/${orgId}/workspaces/${workspaceId}/tasks`)} + className="flex items-center h-10 px-3 gap-2 rounded-lg hover:bg-[#1c1f2e] hover:text-white cursor-pointer transition-colors" + > + + + Issues / Tasks + +
+
+ + + Pull Requests + +
+
+ + + Commits + +
+
navigate(`/organizations/${orgId}/workspaces/${workspaceId}/sprints`)} + className="flex items-center h-10 px-3 gap-2 rounded-lg hover:bg-[#1c1f2e] hover:text-white cursor-pointer transition-colors" + > + + + Sprints + +
+
+ + + CI/CD + +
+
+ + + Wiki + +
+
+ + + Chat + +
+
+ + + Calender + +
+
+ + + Team + +
+
+ + + Settings + +
+
+ )} + {!workspaceId &&
} +
+
(window.location.href = "/organization")} + className={`p-2 flex items-center rounded-lg hover:bg-[#1c1f2e] hover:text-white cursor-pointer ${isCollapsed ? "justify-center" : "gap-2"}`} + > + + + Workspace + +
+
+ + + Profile + +
+ {user?.platformRole === "PLATFORM_ADMIN" && ( +
(window.location.href = "/admin")} + > + + + Admin Panel + +
+ )} +
+
+ + ); +} + +export default Sidebar; + diff --git a/client/src/context/NavigationLoadingContext.jsx b/client/src/context/NavigationLoadingContext.jsx new file mode 100644 index 0000000..565f7b2 --- /dev/null +++ b/client/src/context/NavigationLoadingContext.jsx @@ -0,0 +1,45 @@ +// client/src/context/NavigationLoadingContext.jsx + +import { createContext, useCallback, useContext, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +const NavigationLoadingContext = createContext(null); + +export function NavigationLoadingProvider({ children }) { + const navigate = useNavigate(); + const [isNavigating, setIsNavigating] = useState(false); + + const goTo = useCallback( + (to, options = {}) => { + setIsNavigating(true); + + window.setTimeout(() => { + navigate(to, options); + + // Keep the loader visible briefly after the next page renders. + window.setTimeout(() => { + setIsNavigating(false); + }, 150); + }, 1000); + }, + [navigate] + ); + + return ( + + {children} + + ); +} + +export function useNavigationLoading() { + const context = useContext(NavigationLoadingContext); + + if (!context) { + throw new Error( + "useNavigationLoading must be used inside NavigationLoadingProvider." + ); + } + + return context; +} \ No newline at end of file diff --git a/client/src/context/OrganizationContext.jsx b/client/src/context/OrganizationContext.jsx new file mode 100644 index 0000000..92303da --- /dev/null +++ b/client/src/context/OrganizationContext.jsx @@ -0,0 +1,73 @@ +import React, { createContext, useState, useEffect, useContext } from "react"; +import axios from "axios"; + +const OrganizationContext = createContext(); + +export const OrganizationProvider = ({ children }) => { + const [organizations, setOrganizations] = useState([]); + const [currentOrg, setCurrentOrg] = useState(null); + const [loading, setLoading] = useState(true); + + // Fetch all organizations the logged-in user belongs to + const fetchOrganizations = async () => { + try { + const res = await axios.get( + "http://localhost:5000/api/organizations/mine", + { withCredentials: true }, + ); + if (res.data.success) { + setOrganizations(res.data.data); + + // Remember the last organization they were looking at + const storedOrgId = localStorage.getItem("currentOrgId"); + if (res.data.data.length > 0) { + const orgToSelect = + res.data.data.find((o) => o.id === parseInt(storedOrgId)) || + res.data.data[0]; + setCurrentOrg(orgToSelect); + localStorage.setItem("currentOrgId", orgToSelect.id); + } else { + setCurrentOrg(null); + localStorage.removeItem("currentOrgId"); + } + } + } catch (error) { + console.error("Failed to fetch organizations", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + // Only fetch if a user is currently logged in + if (localStorage.getItem("user")) { + fetchOrganizations(); + } else { + setLoading(false); + } + }, []); + + const switchOrganization = (orgId) => { + const org = organizations.find((o) => o.id === parseInt(orgId)); + if (org) { + setCurrentOrg(org); + localStorage.setItem("currentOrgId", org.id); + } + }; + + return ( + + {children} + + ); +}; + +export const useOrganization = () => useContext(OrganizationContext); diff --git a/client/src/main.jsx b/client/src/main.jsx index b9a1a6d..103ac7c 100644 --- a/client/src/main.jsx +++ b/client/src/main.jsx @@ -1,10 +1,19 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.jsx' +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { GoogleOAuthProvider } from "@react-oauth/google"; +import axios from "axios"; +import { OrganizationProvider } from "./context/OrganizationContext.jsx"; +import "./index.css"; +import App from "./App.jsx"; -createRoot(document.getElementById('root')).render( +axios.defaults.withCredentials = true; + +createRoot(document.getElementById("root")).render( - + + + + + , -) +); \ No newline at end of file diff --git a/client/src/pages/AdminDashboard.jsx b/client/src/pages/AdminDashboard.jsx new file mode 100644 index 0000000..887e3fa --- /dev/null +++ b/client/src/pages/AdminDashboard.jsx @@ -0,0 +1,294 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import { Users, Building2, Settings, Search, ShieldCheck } from "lucide-react"; +import Sidebar from "../components/layout/Sidebar"; +import Navbar from "../components/layout/Navbar"; + +const AdminDashboard = () => { + const [activeTab, setActiveTab] = useState("users"); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + + // Users State + const [users, setUsers] = useState([]); + const [usersLoading, setUsersLoading] = useState(false); + const [usersError, setUsersError] = useState(null); + const [userPage, setUserPage] = useState(1); + const [userTotalPages, setUserTotalPages] = useState(1); + const [userSearch, setUserSearch] = useState(""); + + // Orgs State + const [orgs, setOrgs] = useState([]); + const [orgsLoading, setOrgsLoading] = useState(false); + const [orgsError, setOrgsError] = useState(null); + const [orgPage, setOrgPage] = useState(1); + const [orgTotalPages, setOrgTotalPages] = useState(1); + const [orgSearch, setOrgSearch] = useState(""); + + // Settings State (Static for now) + const [allowSignups, setAllowSignups] = useState(true); + const [maintenanceMode, setMaintenanceMode] = useState(false); + + const fetchUsers = async () => { + setUsersLoading(true); + setUsersError(null); + try { + const response = await axios.get( + `http://localhost:5000/api/admin/users?page=${userPage}&limit=10&search=${userSearch}`, + { withCredentials: true } + ); + setUsers(response.data.data.users); + setUserTotalPages(response.data.data.pagination.totalPages); + } catch (err) { + setUsersError(err.response?.data?.message || "Failed to fetch users"); + } finally { + setUsersLoading(false); + } + }; + + const fetchOrgs = async () => { + setOrgsLoading(true); + setOrgsError(null); + try { + const response = await axios.get( + `http://localhost:5000/api/admin/organizations?page=${orgPage}&limit=10&search=${orgSearch}`, + { withCredentials: true } + ); + setOrgs(response.data.data.organizations); + setOrgTotalPages(response.data.data.pagination.totalPages); + } catch (err) { + setOrgsError(err.response?.data?.message || "Failed to fetch organizations"); + } finally { + setOrgsLoading(false); + } + }; + + useEffect(() => { + if (activeTab === "users") { + const delay = setTimeout(fetchUsers, 500); + return () => clearTimeout(delay); + } + }, [activeTab, userPage, userSearch]); + + useEffect(() => { + if (activeTab === "orgs") { + const delay = setTimeout(fetchOrgs, 500); + return () => clearTimeout(delay); + } + }, [activeTab, orgPage, orgSearch]); + + const renderUsersTab = () => ( +
+
+

Manage Users

+
+ + setUserSearch(e.target.value)} + /> +
+
+ + {usersLoading ? ( +
Loading users...
+ ) : usersError ? ( +
{usersError}
+ ) : users.length === 0 ? ( +
No users found.
+ ) : ( +
+ + + + + + + + + + {users.map((user) => ( + + + + + + ))} + +
UserPlatform RoleJoined
+
+ {user.avatar ? avatar : user.username[0]} +
+
+
{user.username}
+
{user.email}
+
+
+ + {user.platformRole} + + + {new Date(user.createdAt).toLocaleDateString()} +
+
+ + Page {userPage} of {userTotalPages || 1} + +
+
+ )} +
+ ); + + const renderOrgsTab = () => ( +
+
+

Manage Organizations

+
+ + setOrgSearch(e.target.value)} + /> +
+
+ + {orgsLoading ? ( +
Loading organizations...
+ ) : orgsError ? ( +
{orgsError}
+ ) : orgs.length === 0 ? ( +
No organizations found.
+ ) : ( +
+ + + + + + + + + + + {orgs.map((org) => ( + + + + + + + ))} + +
Organization NameStatusTotal MembersCreated
+
{org.name}
+
{org.description || "No description"}
+
+ + ACTIVE + + + {org._count.members} Members + + {new Date(org.createdAt).toLocaleDateString()} +
+
+ + Page {orgPage} of {orgTotalPages || 1} + +
+
+ )} +
+ ); + + const renderSettingsTab = () => ( +
+

Platform Settings

+ +
+
+
+

Allow Public Signups

+

When disabled, new users can only join via invitation.

+
+ +
+ +
+
+

Maintenance Mode

+

Disable access for all non-admin users while performing upgrades.

+
+ +
+ +
+ +
+
+
+ ); + + return ( +
+ +
+ setIsSidebarOpen(!isSidebarOpen)} /> + +
+
+
+ +
+
+

Platform Administration

+

Manage global users, organizations, and system settings.

+
+
+ +
+ + + +
+ + {activeTab === "users" && renderUsersTab()} + {activeTab === "orgs" && renderOrgsTab()} + {activeTab === "settings" && renderSettingsTab()} + +
+
+
+ ); +}; + +export default AdminDashboard; diff --git a/client/src/pages/CreateWorkspaceWizard.jsx b/client/src/pages/CreateWorkspaceWizard.jsx new file mode 100644 index 0000000..6b4cf8d --- /dev/null +++ b/client/src/pages/CreateWorkspaceWizard.jsx @@ -0,0 +1,332 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import axios from "axios"; +import { useOrganization } from "../context/OrganizationContext"; +import { Check, ChevronRight } from "lucide-react"; +import Sidebar from "../components/layout/Sidebar"; +import Navbar from "../components/layout/Navbar"; + +const CreateWorkspaceWizard = () => { + const { currentOrg } = useOrganization(); + const navigate = useNavigate(); + + const [step, setStep] = useState(1); + const [loading, setLoading] = useState(false); + + // Step 1 Data + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [repositoryOption, setRepositoryOption] = useState("CREATE_NEW"); + + // Step 2 Data: CREATE_NEW + const [repoName, setRepoName] = useState(""); + const [repoDesc, setRepoDesc] = useState(""); + const [isPrivate, setIsPrivate] = useState(true); + + // Step 2 Data: CONNECT_EXISTING + const [githubRepos, setGithubRepos] = useState([]); + const [selectedRepoId, setSelectedRepoId] = useState(""); + + // Fetch GitHub repos automatically when they reach Step 2 + useEffect(() => { + if ( + step === 2 && + repositoryOption === "CONNECT_EXISTING" && + githubRepos.length === 0 + ) { + fetchGithubRepos(); + } + }, [step, repositoryOption]); + + const fetchGithubRepos = async () => { + try { + const res = await axios.get( + "http://localhost:5000/api/github/repositories", + { withCredentials: true }, + ); + if (res.data.success) setGithubRepos(res.data.data); + } catch (error) { + console.error("Failed to fetch github repos", error); + } + }; + + const handleNext = () => { + if (step === 1 && !name) return alert("Workspace Name is required!"); + if (step === 2 && repositoryOption === "CREATE_NEW" && !repoName) + return alert("Repository name is required!"); + if ( + step === 2 && + repositoryOption === "CONNECT_EXISTING" && + !selectedRepoId + ) + return alert("Please select a repository!"); + setStep(step + 1); + }; + + const handleSubmit = async () => { + setLoading(true); + try { + let payload = { name, description, repositoryOption }; + + if (repositoryOption === "CREATE_NEW") { + payload.repositoryName = repoName; + payload.repositoryDescription = repoDesc; + payload.isPrivate = isPrivate; + } else { + payload.existingRepo = githubRepos.find( + (r) => r.id.toString() === selectedRepoId.toString(), + ); + } + + // Hit our shiny new transaction API! + const res = await axios.post( + `http://localhost:5000/api/organizations/${currentOrg.id}/workspaces`, + payload, + { withCredentials: true }, + ); + + if (res.data.success) { + // Redirect them to the new Workspace Details page (we will build this next) + navigate( + `/organizations/${currentOrg.id}/workspaces/${res.data.data.id}`, + ); + } + } catch (error) { + alert(error.response?.data?.message || "Failed to create workspace"); + setLoading(false); + } + }; + + const renderStep1 = () => ( +
+

Workspace Details

+
+ + setName(e.target.value)} + placeholder="e.g. Developer Collaboration Platform" + /> +
+
+ +