From 48b69378b6b95fdd82ef5f46bf3ccde7202a0583 Mon Sep 17 00:00:00 2001 From: Alec Lichtenberger Date: Wed, 28 Jan 2026 20:02:32 -0800 Subject: [PATCH 1/9] Added Recharts --- frontend/package-lock.json | 32 ++++++-------------------------- frontend/package.json | 2 +- frontend/src/pages/Alumni.tsx | 2 +- 3 files changed, 8 insertions(+), 28 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c640a9a..5936a63 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -27,7 +27,7 @@ "react-dom": "^18.3.1", "react-icons": "^5.4.0", "react-router-dom": "^6.27.0", - "recharts": "^3.3.0", + "recharts": "^3.7.0", "typescript": "^4.9.5", "vitest": "^3.2.4", "web-vitals": "^2.1.4" @@ -98,7 +98,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -919,7 +918,6 @@ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.13.2.tgz", "integrity": "sha512-jwtMmJa1BXXDCiDx1vC6SFN/+HfYG53UkfJa6qeN5ogvOunzbFDO3wISZy5n9xgYFUrEP6M7e8EG++riHNTv9w==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/component": "0.6.18", "@firebase/logger": "0.4.4", @@ -986,7 +984,6 @@ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.4.2.tgz", "integrity": "sha512-LssbyKHlwLeiV8GBATyOyjmHcMpX/tFjzRUCS1jnwGAew1VsBB4fJowyS5Ud5LdFbYpJeS+IQoC+RQxpK7eH3Q==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/app": "0.13.2", "@firebase/component": "0.6.18", @@ -1002,8 +999,7 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@firebase/auth-compat": { "version": "0.5.28", @@ -1460,7 +1456,6 @@ "integrity": "sha512-zGlBn/9Dnya5ta9bX/fgEoNC3Cp8s6h+uYPYaDieZsFOAdHP/ExzQ/eaDgxD3GOROdPkLKpvKY0iIzr9adle0w==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" }, @@ -2172,7 +2167,8 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2580,7 +2576,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2591,7 +2586,6 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -2665,7 +2659,6 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -3045,7 +3038,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3449,7 +3441,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4112,7 +4103,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -4703,7 +4693,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6206,7 +6195,6 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -6430,6 +6418,7 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -6923,7 +6912,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -7271,7 +7259,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -7284,7 +7271,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -7314,7 +7300,6 @@ "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" @@ -7461,8 +7446,7 @@ "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 + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -8337,7 +8321,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8520,7 +8503,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8662,7 +8644,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -8776,7 +8757,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/frontend/package.json b/frontend/package.json index 6d3ad0e..167e0fa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,7 +22,7 @@ "react-dom": "^18.3.1", "react-icons": "^5.4.0", "react-router-dom": "^6.27.0", - "recharts": "^3.3.0", + "recharts": "^3.7.0", "typescript": "^4.9.5", "vitest": "^3.2.4", "web-vitals": "^2.1.4" diff --git a/frontend/src/pages/Alumni.tsx b/frontend/src/pages/Alumni.tsx index 06f6969..9ed88b2 100644 --- a/frontend/src/pages/Alumni.tsx +++ b/frontend/src/pages/Alumni.tsx @@ -17,7 +17,7 @@ const AlumniProfile: React.FC = () => { const [alumni, setAlumni] = useState(null); const [loading, setLoading] = useState(true); - const [_, setError] = useState(null); + const [error, setError] = useState(null); const handleAlumniUpdate = useCallback(() => { if (!id) return; From ab13e8c796b3bbd822cddbec8399bcd604dda714 Mon Sep 17 00:00:00 2001 From: Alec Lichtenberger Date: Fri, 30 Jan 2026 12:28:44 -0800 Subject: [PATCH 2/9] Backend foundations made, new controller and api routes --- backend/package-lock.json | 165 +++++++++++++++++- backend/package.json | 1 + .../src/controllers/SimilarityController.ts | 123 +++++++++++++ backend/src/controllers/userController.ts | 87 +++++++++ backend/src/routes/userRoutes.ts | 7 + backend/src/util/validateEnv.ts | 1 + frontend/package.json | 3 +- frontend/src/api/users.ts | 22 +++ frontend/src/pages/Alumni.tsx | 4 +- frontend/src/types/Similarity.ts | 9 + 10 files changed, 416 insertions(+), 6 deletions(-) create mode 100644 backend/src/controllers/SimilarityController.ts create mode 100644 frontend/src/types/Similarity.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index ea43016..7b5b1ee 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -19,6 +19,7 @@ "express-async-handler": "^1.2.0", "express-validator": "^7.0.1", "firebase": "^12.5.0", + "groq-sdk": "^0.4.0", "http-errors": "^2.0.0", "module-alias": "^2.2.3", "mongodb": "^5.9.2", @@ -484,13 +485,13 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.2", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.2.tgz", - "integrity": "sha512-Lz1J5IZdTjLYTVIcDP5DVDgi1xlgsF3p1cnvmbfKbjCRhQpftN2e2J4NFfRRvPD54W9+bZ8l5VipPXtTYK7aEg==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.3.tgz", + "integrity": "sha512-iu+JwWHM7tHowKqE+8wNmI3sM6mPEiI9Egscz2BEV7adyKmV95oR9tBO4VIOl72FGDi7X9mXg19VtqIpSkEEsA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.2", - "@aws-sdk/credential-provider-http": "^3.972.3", + "@aws-sdk/credential-provider-http": "^3.972.4", "@aws-sdk/credential-provider-ini": "^3.972.2", "@aws-sdk/credential-provider-process": "^3.972.2", "@aws-sdk/credential-provider-sso": "^3.972.2", @@ -3402,6 +3403,18 @@ "ts-morph": "12.0.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -3449,6 +3462,18 @@ "node": ">=0.4.0" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "8.6.3", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", @@ -5495,6 +5520,15 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/exit-hook": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", @@ -5845,6 +5879,34 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -6131,6 +6193,63 @@ "dev": true, "license": "MIT" }, + "node_modules/groq-sdk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/groq-sdk/-/groq-sdk-0.4.0.tgz", + "integrity": "sha512-h79q9sv4hcOBESR05N5eqHlGhAug9H9lr3EIiB+37ysWWekeG+KYQDK2lIIHYCm6O9LzgZzO/VdLdPP298+T0w==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7", + "web-streams-polyfill": "^3.2.1" + } + }, + "node_modules/groq-sdk/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/groq-sdk/node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/groq-sdk/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "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/groq-sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -6248,6 +6367,15 @@ "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", "license": "MIT" }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -7286,6 +7414,26 @@ "node": ">= 0.6" } }, + "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-fetch": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", @@ -9124,6 +9272,15 @@ "node": ">= 0.8" } }, + "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/web-vitals": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", diff --git a/backend/package.json b/backend/package.json index 6831b42..b394ecf 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,6 +10,7 @@ "express-async-handler": "^1.2.0", "express-validator": "^7.0.1", "firebase": "^12.5.0", + "groq-sdk": "^0.4.0", "http-errors": "^2.0.0", "module-alias": "^2.2.3", "mongodb": "^5.9.2", diff --git a/backend/src/controllers/SimilarityController.ts b/backend/src/controllers/SimilarityController.ts new file mode 100644 index 0000000..b602e0a --- /dev/null +++ b/backend/src/controllers/SimilarityController.ts @@ -0,0 +1,123 @@ +import createHttpError from "http-errors"; +import Groq from "groq-sdk"; + +interface StudentData { + name: string; + school?: string; + fieldOfInterest?: string[]; + projects?: string[]; + hobbies?: string[]; + skills?: string[]; + companiesOfInterest?: string[]; + major?: string; + classLevel?: string; +} + +interface AlumniData { + name: string; + position?: string; + company?: string; + organizations?: string[]; + specializations?: string[]; + hobbies?: string[]; + skills?: string[]; +} + +interface Similarity { + category: string; + description: string; +} + +interface SimilarityResponse { + similarities: Similarity[]; + summary: string; +} + + +export async function analyzeSimilarities( + student: StudentData, + alumni: AlumniData, +): Promise { + const groqApiKey = process.env.GROQ_API_KEY; + + if (!groqApiKey) { + throw createHttpError(500, "Groq API key not configured"); + } + + + const { default: Groq } = await import("groq-sdk"); + const groq = new Groq({ apiKey: groqApiKey }); + + const prompt = ` +You are an expert career mentor analyzing similarities between a student and an alumni. + +STUDENT PROFILE: +- Name: ${student.name} +- School: ${student.school || "Not provided"} +- Major: ${student.major || "Not provided"} +- Class Level: ${student.classLevel || "Not provided"} +- Field of Interest: ${student.fieldOfInterest?.join(", ") || "Not provided"} +- Skills: ${student.skills?.join(", ") || "Not provided"} +- Hobbies: ${student.hobbies?.join(", ") || "Not provided"} +- Projects: ${student.projects?.join(", ") || "Not provided"} +- Companies of Interest: ${student.companiesOfInterest?.join(", ") || "Not provided"} + +ALUMNI PROFILE: +- Name: ${alumni.name} +- Position: ${alumni.position || "Not provided"} +- Company: ${alumni.company || "Not provided"} +- Specializations: ${alumni.specializations?.join(", ") || "Not provided"} +- Skills: ${alumni.skills?.join(", ") || "Not provided"} +- Hobbies: ${alumni.hobbies?.join(", ") || "Not provided"} +- Organizations: ${alumni.organizations?.join(", ") || "Not provided"} + +Please find and list the meaningful similarities between the student and alumni. Focus on: +1. Shared skills +2. Overlapping interests +3. Similar career goals +4. Shared hobbies and passions +5. Similar Educational backgrounds + +Respond in the following JSON format (no markdown, pure JSON): +{ + "similarities": [ + { + "category": "Category Name", + "description": "Brief description of the similarity" + } + ], + "summary": "A brief 2-3 sentence summary of overall similarity and potential mentorship value" +}`; + + try { + const message = await groq.chat.completions.create({ + model: "mixtral-8x7b-32768", + max_tokens: 1024, + messages: [ + { + role: "user", + content: prompt, + }, + ], + }); + + const responseText = + message.choices[0].message.content || ""; + + const jsonMatch = responseText.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw createHttpError(500, "Failed to parse Groq response"); + } + + const similarities: SimilarityResponse = JSON.parse(jsonMatch[0]); + return similarities; + } catch (error) { + if (error instanceof createHttpError.HttpError) { + throw error; + } + throw createHttpError( + 500, + `Error calling Groq API: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } +} \ No newline at end of file diff --git a/backend/src/controllers/userController.ts b/backend/src/controllers/userController.ts index 1f0c56d..9450cca 100644 --- a/backend/src/controllers/userController.ts +++ b/backend/src/controllers/userController.ts @@ -5,6 +5,7 @@ import createHttpError from "http-errors"; import validationErrorParser from "../util/validationErrorParser"; import Company from "../models/Company"; import mongoose from "mongoose"; +import { analyzeSimilarities } from "../controllers/SimilarityController"; interface BaseUserResponse { _id?: string; @@ -323,3 +324,89 @@ export const getOpenAlumni = asyncHandler(async (req, res, next) => { data: users, }); }); + +export const getAlumniSimilarities = asyncHandler(async (req, res, next) => { + const errors = validationResult(req); + if(!errors.isEmpty()){ + return next(createHttpError(400, validationErrorParser(errors))); + } + + const { studentId } = matchedData(req, {locations: ["params"]}); + const { id: alumniId } = matchedData(req, { locations: ["params"] }); + if(!studentId){ + return next( + createHttpError(400, "StudentId is required"), + ); + } + + const [alumniUser, studentUser] = await Promise.all([ + User.findById(alumniId) + .populate({ + path: "company", + model: Company, + }).exec(), + User.findById(studentId).exec(), + + ]); + + if (!alumniUser) { + return next(createHttpError(404, "Alumni user not found.")); + } + + if (!studentUser) { + return next(createHttpError(404, "Student user not found.")); + } + + + if (alumniUser.type !== UserType.Alumni) { + return next(createHttpError(400, "User is not an alumni.")); + } + + if (studentUser.type !== UserType.Student) { + return next(createHttpError(400, "User is not a student.")); + } + + + //Prepare data student and alumni for groq + const StudentData = { + name: studentUser.name, + school: studentUser.school, + fieldOfInterest: studentUser.fieldOfInterest, + projects: studentUser.projects, + hobbies: studentUser.hobbies, + skills: studentUser.skills, + companiesOfInterest: studentUser.companiesOfInterest, + major: studentUser.major, + classLevel: studentUser.classLevel, + } + + const AlumniData = { + name: alumniUser.name, + position: alumniUser.position, + company: (alumniUser.company as any)?.name || "", + organizations: alumniUser.organizations, + specializations: alumniUser.specializations, + hobbies: alumniUser.hobbies, + skills: alumniUser.skills, + } + + + const similarities = await analyzeSimilarities(StudentData, AlumniData); + res.status(200).json({ + student: { + _id: studentUser._id, + name: studentUser.name, + email: studentUser.email, + }, + alumni: { + _id: alumniUser._id, + name: alumniUser.name, + email: alumniUser.email, + position: alumniUser.position, + company: alumniUser.company, + }, + similarities: similarities.similarities, + summary: similarities.summary, + }) +}); + diff --git a/backend/src/routes/userRoutes.ts b/backend/src/routes/userRoutes.ts index 7e03af2..44c00b2 100644 --- a/backend/src/routes/userRoutes.ts +++ b/backend/src/routes/userRoutes.ts @@ -19,6 +19,13 @@ userRouter.get( userController.getUserById, ); +userRouter.get( + "/similarities/:id", + userValidator.getUservalidator, + userController.getAlumniSimilarities, + +) + userRouter.patch( "/:id", preprocessCompany, diff --git a/backend/src/util/validateEnv.ts b/backend/src/util/validateEnv.ts index b2d421a..9fe1df4 100644 --- a/backend/src/util/validateEnv.ts +++ b/backend/src/util/validateEnv.ts @@ -13,4 +13,5 @@ export default cleanEnv(process.env, { AWS_SECRET_ACCESS_KEY: str(), AWS_REGION: str(), AWS_BUCKET_NAME: str(), // Required for S3 uploads and presigned URLs + GROQ_API_KEY: str(), }); diff --git a/frontend/package.json b/frontend/package.json index 167e0fa..9064106 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,7 +25,8 @@ "recharts": "^3.7.0", "typescript": "^4.9.5", "vitest": "^3.2.4", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "groq-sdk": "^0.4.1" }, "scripts": { "dev": "vite", diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index 649f0d3..5e24b1f 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -10,6 +10,7 @@ import { UserType, } from "../types/User"; import { APIResult, get, del, patch, post, handleAPIError } from "./requests"; +import { SimilarityResponse } from "../types/Similarity"; function parseUser(user: UserJSON): User { if (user.type === UserType.Student) { @@ -141,6 +142,8 @@ export async function getAlumni( const response = await get(`/api/users/alumni`, { ...queries, industry: queries.industry?.join(",") || "", + organizations: queries.organizations?.join(",") || "", + specializations: queries.specializations?.join(",") || "", }); const json = (await response.json()) as PaginatedData; const result = { ...json, data: json.data.map(parseAlumni) }; @@ -149,3 +152,22 @@ export async function getAlumni( return handleAPIError(error); } } + + +/** + * Fetch similarities between a student and an alumni from the backend + * + * @param id The ID of the alumni to compare with + * @returns SimilarityResponse containing similarities and summary + */ +export async function getSimilarities( + id: string, +): Promise> { + try { + const response = await get(`/api/users/similarities/${id}`); + const json = (await response.json()) as SimilarityResponse; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} diff --git a/frontend/src/pages/Alumni.tsx b/frontend/src/pages/Alumni.tsx index 9ed88b2..ccf2245 100644 --- a/frontend/src/pages/Alumni.tsx +++ b/frontend/src/pages/Alumni.tsx @@ -36,7 +36,9 @@ const AlumniProfile: React.FC = () => { }, [id]); // Initial company fetch - useEffect(() => { handleAlumniUpdate(); }, [handleAlumniUpdate]); + useEffect(() => { + handleAlumniUpdate(); + }, [handleAlumniUpdate]); if (!alumni) return ( diff --git a/frontend/src/types/Similarity.ts b/frontend/src/types/Similarity.ts new file mode 100644 index 0000000..ffbc32a --- /dev/null +++ b/frontend/src/types/Similarity.ts @@ -0,0 +1,9 @@ +export interface Similarity { + category: string; + description: string; +} + +export interface SimilarityResponse { + similarities: Similarity[]; + summary: string; +} From 0ae7c1c18af692f66b3e9cc7a15f3a03e06f4ef2 Mon Sep 17 00:00:00 2001 From: Alec Lichtenberger Date: Fri, 30 Jan 2026 12:42:21 -0800 Subject: [PATCH 3/9] Frontend api made, Alumni.tsx can send and accept a result json --- frontend/src/pages/Alumni.tsx | 41 ++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/Alumni.tsx b/frontend/src/pages/Alumni.tsx index ccf2245..e8b7d9e 100644 --- a/frontend/src/pages/Alumni.tsx +++ b/frontend/src/pages/Alumni.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState, useRef } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { FaArrowLeft, @@ -6,10 +6,12 @@ import { } from "react-icons/fa"; import { LuMail, LuBuilding2, LuBriefcase } from "react-icons/lu"; import { FiPhone } from "react-icons/fi"; -import { getAlumniById } from "../api/users"; +import { getAlumniById, getSimilarities } from "../api/users"; import { APIResult } from "../api/requests"; import { Alumni } from "../types/User"; import { ProgressSpinner } from "primereact/progressspinner"; +import { useAuth } from "../contexts/useAuth"; +import { Toast } from "primereact/toast"; const AlumniProfile: React.FC = () => { const { id } = useParams<{ id: string }>(); @@ -18,6 +20,8 @@ const AlumniProfile: React.FC = () => { const [alumni, setAlumni] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const { user, isAuthenticated } = useAuth(); + const toast = useRef(null); const handleAlumniUpdate = useCallback(() => { if (!id) return; @@ -37,7 +41,38 @@ const AlumniProfile: React.FC = () => { // Initial company fetch useEffect(() => { - handleAlumniUpdate(); + handleAlumniUpdate(); + + const resolveUserId = (): string | null => { + if (user) { + return user._id ?? null; + } + return null; + }; + + const fetchSimilarities = async () => { + try{ + const userId = resolveUserId(); + if(!userId){ + console.error("User not found:", {isAuthenticated, user }); + return; + } + + const res = await getSimilarities(userId); + + if(!res.success){ + throw new Error ("Failed to fetch application data"); + } + } catch(err){ + const error = err as Error; + console.error("Error fetching alumni similarities:", error.message); + toast.current?.show({ + severity: "error", + summary: "Error", + detail: `Failed to update similarities: ${error.message || "Unknown error"}`, + }); + } + } }, [handleAlumniUpdate]); if (!alumni) From 8fcd8566e162430e51e5c632edc29c5c5d9c864d Mon Sep 17 00:00:00 2001 From: cthotti <68891531+cthotti@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:26:58 -0800 Subject: [PATCH 4/9] Fixing and implementing similarity feature. Redesigned student and alumni profiles + connect page --- .../src/controllers/SimilarityController.ts | 95 +- backend/src/controllers/userController.ts | 5 +- backend/src/models/User.ts | 5 +- backend/src/routes/userRoutes.ts | 4 +- backend/src/validators/userValidator.ts | 15 + frontend/src/api/users.ts | 5 +- .../src/components/connect/AlumniTile.tsx | 79 +- frontend/src/index.css | 2 + frontend/src/pages/Alumni.tsx | 582 +++++---- frontend/src/pages/Connect.tsx | 126 +- frontend/src/pages/Profile.tsx | 1107 +++++++++-------- frontend/src/styles/Animations.css | 453 +++++++ 12 files changed, 1584 insertions(+), 894 deletions(-) create mode 100644 frontend/src/styles/Animations.css diff --git a/backend/src/controllers/SimilarityController.ts b/backend/src/controllers/SimilarityController.ts index b602e0a..d367602 100644 --- a/backend/src/controllers/SimilarityController.ts +++ b/backend/src/controllers/SimilarityController.ts @@ -44,55 +44,56 @@ export async function analyzeSimilarities( throw createHttpError(500, "Groq API key not configured"); } - - const { default: Groq } = await import("groq-sdk"); const groq = new Groq({ apiKey: groqApiKey }); const prompt = ` -You are an expert career mentor analyzing similarities between a student and an alumni. - -STUDENT PROFILE: -- Name: ${student.name} -- School: ${student.school || "Not provided"} -- Major: ${student.major || "Not provided"} -- Class Level: ${student.classLevel || "Not provided"} -- Field of Interest: ${student.fieldOfInterest?.join(", ") || "Not provided"} -- Skills: ${student.skills?.join(", ") || "Not provided"} -- Hobbies: ${student.hobbies?.join(", ") || "Not provided"} -- Projects: ${student.projects?.join(", ") || "Not provided"} -- Companies of Interest: ${student.companiesOfInterest?.join(", ") || "Not provided"} - -ALUMNI PROFILE: -- Name: ${alumni.name} -- Position: ${alumni.position || "Not provided"} -- Company: ${alumni.company || "Not provided"} -- Specializations: ${alumni.specializations?.join(", ") || "Not provided"} -- Skills: ${alumni.skills?.join(", ") || "Not provided"} -- Hobbies: ${alumni.hobbies?.join(", ") || "Not provided"} -- Organizations: ${alumni.organizations?.join(", ") || "Not provided"} - -Please find and list the meaningful similarities between the student and alumni. Focus on: -1. Shared skills -2. Overlapping interests -3. Similar career goals -4. Shared hobbies and passions -5. Similar Educational backgrounds - -Respond in the following JSON format (no markdown, pure JSON): -{ - "similarities": [ + You are an expert career mentor analyzing similarities between a student and an alumni. + + STUDENT PROFILE: + - Name: ${student.name} + - School: ${student.school || "Not provided"} + - Major: ${student.major || "Not provided"} + - Class Level: ${student.classLevel || "Not provided"} + - Field of Interest: ${student.fieldOfInterest?.join(", ") || "Not provided"} + - Skills: ${student.skills?.join(", ") || "Not provided"} + - Hobbies: ${student.hobbies?.join(", ") || "Not provided"} + - Projects: ${student.projects?.join(", ") || "Not provided"} + - Companies of Interest: ${student.companiesOfInterest?.join(", ") || "Not provided"} + + ALUMNI PROFILE: + - Name: ${alumni.name} + - Position: ${alumni.position || "Not provided"} + - Company: ${alumni.company || "Not provided"} + - Specializations: ${alumni.specializations?.join(", ") || "Not provided"} + - Skills: ${alumni.skills?.join(", ") || "Not provided"} + - Hobbies: ${alumni.hobbies?.join(", ") || "Not provided"} + - Organizations: ${alumni.organizations?.join(", ") || "Not provided"} + + List the key similarities between the students and alumni in a single line. Don't use + complete sentences and no need to focus on grammer. If the two don't have similarities + on a specific topic, then don't mention it. Focus on: + 1. Shared skills + 2. Overlapping interests + 3. Similar career goals + 4. Shared hobbies and passions + 5. Similar Educational backgrounds + + Respond in the following JSON format (no markdown, pure JSON): { - "category": "Category Name", - "description": "Brief description of the similarity" - } - ], - "summary": "A brief 2-3 sentence summary of overall similarity and potential mentorship value" -}`; + "similarities": [ + { + "category": "Category Name", + "description": "Brief description of the similarity" + } + ], + "summary": "A brief 1-2 sentence summary of overall similarity and potential mentorship value" + }`; try { const message = await groq.chat.completions.create({ - model: "mixtral-8x7b-32768", + model: "llama-3.1-8b-instant", max_tokens: 1024, + response_format: {type: "json_object"}, messages: [ { role: "user", @@ -101,17 +102,23 @@ Respond in the following JSON format (no markdown, pure JSON): ], }); - const responseText = - message.choices[0].message.content || ""; + const responseText = message.choices[0].message.content || ""; const jsonMatch = responseText.match(/\{[\s\S]*\}/); if (!jsonMatch) { throw createHttpError(500, "Failed to parse Groq response"); } - const similarities: SimilarityResponse = JSON.parse(jsonMatch[0]); + let similarities: SimilarityResponse; + try { + similarities = JSON.parse(responseText); + } catch (e) { + console.error("JSON.parse failed on Groq content:", responseText); + throw createHttpError(500, "Groq returned invalid JSON"); + } return similarities; } catch (error) { + console.error("error in analyze similarities: ", error); if (error instanceof createHttpError.HttpError) { throw error; } diff --git a/backend/src/controllers/userController.ts b/backend/src/controllers/userController.ts index 9450cca..f4dda60 100644 --- a/backend/src/controllers/userController.ts +++ b/backend/src/controllers/userController.ts @@ -4,7 +4,6 @@ import asyncHandler from "express-async-handler"; import createHttpError from "http-errors"; import validationErrorParser from "../util/validationErrorParser"; import Company from "../models/Company"; -import mongoose from "mongoose"; import { analyzeSimilarities } from "../controllers/SimilarityController"; interface BaseUserResponse { @@ -31,7 +30,7 @@ interface StudentResponse extends BaseUserResponse { interface AlumniResponse extends BaseUserResponse { linkedIn?: string; phoneNumber?: string; - company?: mongoose.Types.ObjectId; + company?: string; shareProfile?: boolean; position?: string; organizations?: string[]; @@ -383,7 +382,7 @@ export const getAlumniSimilarities = asyncHandler(async (req, res, next) => { const AlumniData = { name: alumniUser.name, position: alumniUser.position, - company: (alumniUser.company as any)?.name || "", + company: alumniUser.company, organizations: alumniUser.organizations, specializations: alumniUser.specializations, hobbies: alumniUser.hobbies, diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts index 1cdd5a0..99de71f 100644 --- a/backend/src/models/User.ts +++ b/backend/src/models/User.ts @@ -89,8 +89,9 @@ const userSchema = new Schema({ trim: true, }, company: { - type: Schema.Types.ObjectId, + type: String, required: false, + trim: true, }, shareProfile: { type: Boolean, @@ -108,7 +109,7 @@ const userSchema = new Schema({ // Add indexes for better performance userSchema.index({ type: 1, shareProfile: 1 }); // Alumni search -userSchema.index({ company: 1 }); // Company filtering +userSchema.index({ company: "text" }); // Company filtering userSchema.index({ name: "text" }); // Name search userSchema.index({ position: "text" }); // Position search diff --git a/backend/src/routes/userRoutes.ts b/backend/src/routes/userRoutes.ts index 44c00b2..d643711 100644 --- a/backend/src/routes/userRoutes.ts +++ b/backend/src/routes/userRoutes.ts @@ -20,8 +20,8 @@ userRouter.get( ); userRouter.get( - "/similarities/:id", - userValidator.getUservalidator, + "/similarities/:studentId/:id", + userValidator.getSimilaritiesValidator, userController.getAlumniSimilarities, ) diff --git a/backend/src/validators/userValidator.ts b/backend/src/validators/userValidator.ts index 7010abe..d88017f 100644 --- a/backend/src/validators/userValidator.ts +++ b/backend/src/validators/userValidator.ts @@ -297,3 +297,18 @@ export const getOpenAlumniValidator = [ validatePositionQuery, validateIndustry, ]; + +export const getSimilaritiesValidator = [ + param("studentId") + .isString() + .withMessage("studentId must be a string.") + .trim() + .isLength({ min: 1 }) + .withMessage("studentId is required."), + param("id") + .isString() + .withMessage("alumni id must be a string.") + .trim() + .isLength({ min: 1 }) + .withMessage("alumni id is required."), +]; \ No newline at end of file diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index 5e24b1f..cd3aaaf 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -161,10 +161,11 @@ export async function getAlumni( * @returns SimilarityResponse containing similarities and summary */ export async function getSimilarities( - id: string, + studentId: string, + alumniId: string, ): Promise> { try { - const response = await get(`/api/users/similarities/${id}`); + const response = await get(`/api/users/similarities/${studentId}/${alumniId}`); const json = (await response.json()) as SimilarityResponse; return { success: true, data: json }; } catch (error) { diff --git a/frontend/src/components/connect/AlumniTile.tsx b/frontend/src/components/connect/AlumniTile.tsx index 6265c3a..3c76cd9 100644 --- a/frontend/src/components/connect/AlumniTile.tsx +++ b/frontend/src/components/connect/AlumniTile.tsx @@ -21,11 +21,25 @@ const AlumniTile: React.FC = ({ data }) => { }; return ( -
navigate(`/alumni/${data._id}`)} - className="bg-white rounded-lg overflow-visible h-auto transition border border-gray-300 shadow-sm hover:shadow-md"> - {/* Card header with avatar and name */} -
-
+
navigate(`/alumni/${data._id}`)} + className=" + bg-zinc-900/80 + backdrop-blur + rounded-xl + overflow-visible + h-auto + border + border-zinc-800 + shadow-lg + transition + hover:shadow-indigo-500/10 + hover:border-zinc-700 + " + > + {/* Card header */} +
+
{data.profilePicture ? ( = ({ data }) => { className="w-full h-full object-cover" /> ) : ( - {data.name.charAt(0)} + + {data.name.charAt(0)} + )}
-

{data.name}

+

+ {data.name} +

- {/* Card body with user details */} + {/* Card body */}
- -
+ +
{data.email} @@ -56,37 +74,60 @@ const AlumniTile: React.FC = ({ data }) => {
- -
+ +
{data.company ? ( {data.company.name} ) : ( - Not specified + Not specified )}
- -
- {data.position ? data.position : Not specified} + +
+ {data.position ? ( + data.position + ) : ( + Not specified + )}
+ {/* Animated button */}
+ { const { id } = useParams<{ id: string }>(); + console.log("route param id: ", id); const navigate = useNavigate(); const [alumni, setAlumni] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const { user, isAuthenticated } = useAuth(); - const toast = useRef(null); + const { user} = useAuth(); + + const [similarities, setSimilarities] = useState(null); + const [similaritySummary, setSimilaritySummary] = useState(null); + const [similarityLoading, setSimilarityLoading] = useState(false); + const [showSimilarities, setShowSimilarities] = useState(false); const handleAlumniUpdate = useCallback(() => { if (!id) return; @@ -33,6 +40,7 @@ const AlumniProfile: React.FC = () => { setError(null); } else { setError(result.error); + console.log(error); } }) .catch((e) => setError(e instanceof Error ? e.message : "Unknown error")) @@ -42,8 +50,9 @@ const AlumniProfile: React.FC = () => { // Initial company fetch useEffect(() => { handleAlumniUpdate(); - - const resolveUserId = (): string | null => { + }, [handleAlumniUpdate]); + + const resolveUserId = (): string | null => { if (user) { return user._id ?? null; } @@ -51,265 +60,380 @@ const AlumniProfile: React.FC = () => { }; const fetchSimilarities = async () => { - try{ - const userId = resolveUserId(); - if(!userId){ - console.error("User not found:", {isAuthenticated, user }); - return; - } + if (!id) return; - const res = await getSimilarities(userId); + const userId = resolveUserId(); + if (!userId) { + return; + } + + try { + setSimilarityLoading(true); + const res = await getSimilarities(userId, id); - if(!res.success){ - throw new Error ("Failed to fetch application data"); + if (!res.success) { + throw new Error("Failed to fetch similarities"); } - } catch(err){ - const error = err as Error; - console.error("Error fetching alumni similarities:", error.message); - toast.current?.show({ - severity: "error", - summary: "Error", - detail: `Failed to update similarities: ${error.message || "Unknown error"}`, - }); + + setSimilarities(res.data.similarities); + setSimilaritySummary(res.data.summary); + setShowSimilarities(true); + } catch (err) { + console.error(err); + } finally { + setSimilarityLoading(false); } - } - }, [handleAlumniUpdate]); + }; + if (!alumni) return ( -
- Alumni not found. +
+

Alumni not found.

); return ( -
+
{/* Display Spinner While Loading */} {loading && (
- +
)} + {/* When Finished Loading */} {!loading && ( -
-
- {/* Back Button to Exit Profile */} +
+ {/* Back Button */} +
-
-
-
- {/* Left Column - Profile Image */} -
-
- {alumni.profilePicture ? ( - {alumni.name} - ) : ( -
- {alumni.name - .split(" ") - .map((n) => n[0]) - .join("") - .toUpperCase()} -
- )} + +
+ {/* Horizontal Profile Header Card */} +
+ {/* Top gradient border */} +
+ +
+ {/* Profile Image */} +
+ {alumni.profilePicture ? ( + {alumni.name} + ) : ( +
+ {alumni.name + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase()} +
+ )} +
+
+ + {/* Name and Basic Info */} +
+

{alumni.name}

+

Alumni

+ + {/* Contact Info - Horizontal Layout */} +
+
+
+ Email +
+
+ {alumni.email}
- - {/* Right Column - User Information */} -
- {/* Basic Information */} -
-

- Basic Information -

-
-
- -

{alumni.name}

-
-
- -

{alumni.email}

-
- -
- -

- {alumni.phoneNumber || "Not provided"} -

-
-
+ +
+
+ Phone
- - {/* LinkedIn */} -
- - {alumni.linkedIn ? ( - - {alumni.linkedIn} - - ) : ( -

Not provided

- )} +
+ {alumni.phoneNumber || Not provided}
- - - {/* Alumni-specific Information */} -
-
-

- Professional Information -

-
-
- -

- {alumni.company?.name || "Not specified"} -

-
- -
- -

- {alumni.position || "Not specified"} -

-
-
-
-
- - {Array.isArray(alumni.organizations) && alumni.organizations.length > 0 ? ( -
- {alumni.organizations.map((organization, index) => ( - - {organization.charAt(0).toUpperCase() + organization.slice(1).toLowerCase()} - - ))} -
- ) : ( -

Not specified

- )} -
- -
- - {Array.isArray(alumni.specializations) && alumni.specializations.length > 0 ? ( -
- {alumni.specializations.map((specialization, index) => ( - - {specialization.charAt(0).toUpperCase() + specialization.slice(1).toLowerCase()} - - ))} -
- ) : ( -

Not specified

- )} -
- -
- - {Array.isArray(alumni.hobbies) && alumni.hobbies.length > 0 ? ( -
- {alumni.hobbies.map((hobby, index) => ( - - {hobby.charAt(0).toUpperCase() + hobby.slice(1).toLowerCase()} - - ))} -
- ) : ( -

Not specified

- )} -
- +
+ +
+
+ LinkedIn +
+ {alumni.linkedIn ? ( + + View Profile → + + ) : ( +
+ Not provided +
+ )} +
+
+
+
+
+ + {/* Professional Information - Horizontal */} +
+

+
+ Professional Information +

+
+
+
+ Company +
+
+ {alumni.company?.name || Not specified} +
+
+ +
+
+ Position +
+
+ {alumni.position || Not specified} +
+
+
+
+ + {/* Organizations - Horizontal */} +
+

+
+ Organizations +

+ {Array.isArray(alumni.organizations) && alumni.organizations.length > 0 ? ( +
+ {alumni.organizations.map((organization, index) => ( + + {organization.charAt(0).toUpperCase() + organization.slice(1).toLowerCase()} + + ))} +
+ ) : ( +
+ Not specified +
+ )} +
+ + {/* Specializations - Horizontal */} +
+

+
+ Specializations +

+ {Array.isArray(alumni.specializations) && alumni.specializations.length > 0 ? ( +
+ {alumni.specializations.map((specialization, index) => ( + + {specialization.charAt(0).toUpperCase() + specialization.slice(1).toLowerCase()} + + ))} +
+ ) : ( +
+ Not specified +
+ )} +
+ + {/* Skills - Horizontal */} +
+

+
+ Technical Skills +

+ {Array.isArray(alumni.skills) && alumni.skills.length > 0 ? ( +
+ {alumni.skills.map((skill, index) => ( + + {skill.charAt(0).toUpperCase() + skill.slice(1).toLowerCase()} + + ))} +
+ ) : ( +
+ Not specified +
+ )} +
+ + {/* Hobbies - Horizontal */} +
+

+
+ Hobbies & Interests +

+ {Array.isArray(alumni.hobbies) && alumni.hobbies.length > 0 ? ( +
+ {alumni.hobbies.map((hobby, index) => ( + + {hobby.charAt(0).toUpperCase() + hobby.slice(1).toLowerCase()} + + ))} +
+ ) : ( +
+ Not specified +
+ )} +
+ + {/* AI-Powered Similarities Section - Horizontal */} +
+
+

+
+ AI Insights +

+ +
+ + {/* Similarities Display - Horizontal Special AI Box */} + {showSimilarities && similarities && ( +
+ {/* AI-Powered Badge */} +
+ + AI-Generated Insights +
+ + {/* Similarities List with Special Styling - Horizontal Grid */} +
+ {/* Corner decorations */} +
+
+
+
+ +

+ Common Ground +

+ + {/* Horizontal Grid Layout */} +
+ {similarities.map((sim, idx) => ( +
+
- - {Array.isArray(alumni.skills) && alumni.skills.length > 0 ? ( -
- {alumni.skills.map((skill, index) => ( - - {skill.charAt(0).toUpperCase() + skill.slice(1).toLowerCase()} - - ))} -
- ) : ( -

Not specified

- )} + {sim.category} + {sim.description}
+ ))}
+ + {similaritySummary && ( +
+

+ "{similaritySummary}" +

+
+ )}
-
+ )} + + {/* Empty State */} + {!showSimilarities && !similarityLoading && ( +
+
+ +
+

+ Click "Find Match" to discover what you have in common with this alumni +

+
+ )} +
+
)}
); }; -export default AlumniProfile; +export default AlumniProfile; \ No newline at end of file diff --git a/frontend/src/pages/Connect.tsx b/frontend/src/pages/Connect.tsx index b71e787..ee4cd68 100644 --- a/frontend/src/pages/Connect.tsx +++ b/frontend/src/pages/Connect.tsx @@ -4,8 +4,9 @@ import DataList from "../components/public/DataList"; import AlumniTile from "../components/connect/AlumniTile"; import { getAlumni } from "../api/users"; import { Alumni } from "../types/User"; -import { PaginatedData } from "../types/PaginatedData"; import { IndustryType } from "../types/Company"; +import { LuUsers, LuSearch } from "react-icons/lu"; +import "../styles/Animations.css"; interface SearchBarData extends Record { query: string; @@ -18,66 +19,103 @@ const Connect = () => { industry: [], }); - // fetch paginated alumni whenever search options change const getPaginatedOpenAlumni = useCallback( async (page: number, perPage: number) => { const res = await getAlumni({ - page: page, - perPage: perPage, - query: search.query.length >= 1 ? search.query : undefined, + page, + perPage, + query: search.query || undefined, industry: search.industry, }); - if (res.success) { - return { - ...res.data, - }; - } else { - console.error(res.error); - - return { - page: 0, - perPage: 0, - total: 0, - data: [], - } as PaginatedData; - } + return res.success + ? res.data + : { page: 0, perPage: 0, total: 0, data: [] }; }, - [search], + [search] ); return ( -
-
+
+
+ {/* Header */} -
-

Alumni Directory

-

Connect with UCSD alumni working at your dream companies

+
+
+
+ +
+
+

+ Alumni Directory +

+

+ Connect with UCSD alumni at your dream companies +

+
+
-
- - selections={[ - { - label: "Industry", - options: [...Object.values(IndustryType)], - }, - ]} - placeholder="Search by name, company, or position" - onSubmitForm={setSearch} - /> + + {/* Search Card */} +
+
+
+ +

+ Find Your Connection +

+
+ +
+ + selections={[ + { label: "Industry", options: Object.values(IndustryType) }, + ]} + placeholder="Search by name, company, or position" + onSubmitForm={setSearch} + /> +
+
- {/* Alumni List */} -
-
+ + {/* Alumni Grid */} +
+
+

+ Browse Alumni +

+ +
+ + Instant Results +
+
+ + {/* Grid wrapper adds shading + hover context */} +
+
+ - pageType = "connect" - key={`${search.query}_${search.industry.join(',')}`} + pageType="connect" + key={`${search.query}_${search.industry.join(",")}`} fetchData={getPaginatedOpenAlumni} useServerPagination - listStyle={{}} - listClassName="grid grid-cols-3 gap-4" + listClassName=" + relative z-10 + grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 + gap-8 + + [&_.pagination]:text-white + [&_.pagination_*]:text-white + [&_select]:text-white + [&_option]:text-black + " paginatorContent={{ setPerPage: true, goToPage: true }} - TileComponent={AlumniTile} + TileComponent={(props) => ( +
+ +
+ )} />
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index b306d03..1c8100d 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import { LuGraduationCap, LuBriefcase, @@ -20,6 +20,7 @@ import { } from "../types/User"; import { Dropdown } from "primereact/dropdown"; import CompanyDropdown from "../components/company/CompanyDropdown"; +import "../styles/Animations.css"; const Profile = () => { const classLevelOptions = Object.keys(ClassLevel).map((key) => ({ @@ -29,7 +30,6 @@ const Profile = () => { const { user, updateUser } = useAuth(); - const [studentProfile, setStudentProfile] = useState<{ _id?: string; userId: string }>({ userId: user?._id ? String(user._id) : "" }); const [alumniProfile, setAlumniProfile] = useState<{ _id?: string; userId: string }>({ userId: user?._id ? String(user._id) : "" }); @@ -94,7 +94,6 @@ const Profile = () => { } else if (updatedUser.type === UserType.Alumni) { saveAlumniProfile(); } - }); }; @@ -108,7 +107,6 @@ const Profile = () => { const saveStudentProfile = () => { if (!studentProfile) return; if (!studentProfile._id) { - // No profile in db, then create profile fetch(`/api/profile/student`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -120,7 +118,6 @@ const Profile = () => { console.log("created student profile", data); }); } else { - // updating profile fetch(`/api/profile/student/${user?._id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, @@ -137,7 +134,6 @@ const Profile = () => { const saveAlumniProfile = () => { if (!alumniProfile) return; if (!alumniProfile._id) { - // No profile in db, then create profile fetch(`/api/profile/alumni`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -149,7 +145,6 @@ const Profile = () => { console.log("created alumni profile", data); }); } else { - // updating profile fetch(`/api/profile/alumni/${user?._id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, @@ -194,7 +189,7 @@ const Profile = () => { const onPhoneNumberChanged = (e: React.ChangeEvent) => { const input = e.target.value; - const cleanedInput = input.replace(/\D/g, ""); // Remove non-digits + const cleanedInput = input.replace(/\D/g, ""); let formattedNumber = ""; @@ -202,14 +197,9 @@ const Profile = () => { if (cleanedInput.length < 4) { formattedNumber = `(${cleanedInput}`; } else if (cleanedInput.length < 7) { - formattedNumber = `(${cleanedInput.slice(0, 3)}) ${cleanedInput.slice( - 3, - )}`; + formattedNumber = `(${cleanedInput.slice(0, 3)}) ${cleanedInput.slice(3)}`; } else { - formattedNumber = `(${cleanedInput.slice(0, 3)}) ${cleanedInput.slice( - 3, - 6, - )}-${cleanedInput.slice(6, 10)}`; + formattedNumber = `(${cleanedInput.slice(0, 3)}) ${cleanedInput.slice(3, 6)}-${cleanedInput.slice(6, 10)}`; } handleInputChange("phoneNumber", formattedNumber); } else { @@ -226,14 +216,20 @@ const Profile = () => { } return ( -
-
-
-

User Profile

+
+
+ {/* Header */} +
+
+

+ Profile +

+

Manage your academic and professional presence

+
{!isEditing && (
-
- {/* Left Column - Profile Image */} -
-
+ {/* Main Grid */} +
+ {/* Left Sidebar - Profile Card */} +
+ {/* Top gradient border */} +
+ + {/* Profile Image */} +
{user.profilePicture ? ( {user.name} ) : ( -
+
{user.name .split(" ") .map((n) => n[0]) @@ -270,544 +273,557 @@ const Profile = () => { .toUpperCase()}
)} +
-
- {/* Right Column - User Information */} -
- {/* Basic Information */} -
-

- Basic Information -

-
-
- -

{user.name}

+ {/* Profile Fields */} +
+
+
+ 👤 Full Name +
+
+ {user.name || Not provided}
+
-
- - {isEditing ? ( -
- - -
- ) : ( -

- {user.type === UserType.Student ? "Student" : "Alumni"} -

- )} +
+
+ Email +
+
+ {user.email || Not provided}
+
-
- -

{user.email}

+
+
+ Phone Number
+ {isEditing ? ( + + ) : ( +
+ {user.phoneNumber || Not provided} +
+ )} +
-
- - {isEditing ? ( - - ) : ( -

- {user.phoneNumber || "Not provided"} -

- )} +
+
+ 👔 User Type
+ {isEditing ? ( +
+ + +
+ ) : ( +
+ {user.type === UserType.Student ? "Student" : "Alumni"} +
+ )}
-
- {/* LinkedIn */} -
-
+ {/* Right Content Area */} +
{/* Student-specific Information */} {user.type === UserType.Student && !isEditing && ( -
-

- Academic Information -

-
-
-