From f15125815fe67fb1f4bc3a2752f6356cafa81eac Mon Sep 17 00:00:00 2001 From: Andrew Rafal Date: Tue, 24 Feb 2026 17:48:37 -0800 Subject: [PATCH 1/4] Added new Students page, not populated yet. --- backend/package-lock.json | 17 +- backend/seed-data/seed-students.ts | 10 + frontend/package-lock.json | 30 +- frontend/src/App.tsx | 4 + frontend/src/api/users.ts | 51 +- .../connect/StudentProfileModal.tsx | 154 +++++ .../src/components/connect/StudentTile.tsx | 98 ++++ frontend/src/components/public/DataList.tsx | 4 +- frontend/src/constants/navItems.ts | 5 + frontend/src/pages/Student.tsx | 540 ++++++++++++++++++ frontend/src/pages/Students.tsx | 88 +++ frontend/src/types/User.ts | 33 +- 12 files changed, 1023 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/connect/StudentProfileModal.tsx create mode 100644 frontend/src/components/connect/StudentTile.tsx create mode 100644 frontend/src/pages/Student.tsx create mode 100644 frontend/src/pages/Students.tsx diff --git a/backend/package-lock.json b/backend/package-lock.json index 7ecd772..48611eb 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1263,6 +1263,7 @@ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.8.tgz", "integrity": "sha512-WiE9uCGRLUnShdjb9iP20sA3ToWrBbNXr14/N5mow7Nls9dmKgfGaGX5cynLvrltxq2OrDLh1VDNaUgsnS/k/g==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", @@ -1329,6 +1330,7 @@ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.8.tgz", "integrity": "sha512-4De6SUZ36zozl9kh5rZSxKWULpgty27rMzZ6x+xkoo7+NWyhWyFdsdvhFsWhTw/9GGj0wXIcbTjwHYCUIUuHyg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@firebase/app": "0.14.8", "@firebase/component": "0.7.0", @@ -1344,7 +1346,8 @@ "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" + "license": "Apache-2.0", + "peer": true }, "node_modules/@firebase/auth": { "version": "1.12.0", @@ -1795,6 +1798,7 @@ "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" }, @@ -2943,6 +2947,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3040,6 +3045,7 @@ "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", "@typescript-eslint/scope-manager": "6.21.0", @@ -3076,6 +3082,7 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -3281,7 +3288,8 @@ "version": "14.18.33", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.33.tgz", "integrity": "sha512-qelS/Ra6sacc4loe/3MSjXNL1dNQ/GjxNHVzuChwMfmk7HuycRLVQN2qNY3XahK+fZc5E2szqQSKUyAF0E+2bg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@vercel/node/node_modules/ts-node": { "version": "10.9.1", @@ -3331,6 +3339,7 @@ "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" @@ -3380,6 +3389,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4961,6 +4971,7 @@ "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", @@ -5126,6 +5137,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -9134,6 +9146,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/backend/seed-data/seed-students.ts b/backend/seed-data/seed-students.ts index 9f290cb..940431e 100644 --- a/backend/seed-data/seed-students.ts +++ b/backend/seed-data/seed-students.ts @@ -22,6 +22,7 @@ const preservedUserIds: string[] = []; const sampleStudents = [ { + _id: "student_001", email: "alex.chen@university.edu", name: "Alex Chen", profilePicture: @@ -40,6 +41,7 @@ const sampleStudents = [ school: "Tech University", }, { + _id: "student_002", email: "maya.patel@state.edu", name: "Maya Patel", profilePicture: @@ -58,6 +60,7 @@ const sampleStudents = [ school: "State University", }, { + _id: "student_003", email: "jordan.smith@college.edu", name: "Jordan Smith", profilePicture: @@ -76,6 +79,7 @@ const sampleStudents = [ school: "Institute of Technology", }, { + _id: "student_004", email: "liam.nguyen@uni.edu", name: "Liam Nguyen", profilePicture: @@ -94,6 +98,7 @@ const sampleStudents = [ school: "Academy of Arts", }, { + _id: "student_005", email: "sarah.kim@global.edu", name: "Sarah Kim", profilePicture: @@ -112,6 +117,7 @@ const sampleStudents = [ school: "Global Business School", }, { + _id: "student_006", email: "oscar.rodriguez@tech.edu", name: "Oscar Rodriguez", profilePicture: @@ -130,6 +136,7 @@ const sampleStudents = [ school: "Design Institute", }, { + _id: "student_007", email: "chloe.wilson@state.edu", name: "Chloe Wilson", profilePicture: "https://images.unsplash.com/photo-1544005313-94ddf0286df2", @@ -147,6 +154,7 @@ const sampleStudents = [ school: "Western University", }, { + _id: "student_008", email: "ethan.brown@poly.edu", name: "Ethan Brown", profilePicture: @@ -165,6 +173,7 @@ const sampleStudents = [ school: "Polytechnic University", }, { + _id: "student_009", email: "isabella.white@uni.edu", name: "Isabella White", profilePicture: "https://images.unsplash.com/photo-1554151228-14d9def656e4", @@ -182,6 +191,7 @@ const sampleStudents = [ school: "Central University", }, { + _id: "student_10", email: "noah.davis@college.edu", name: "Noah Davis", profilePicture: "https://images.unsplash.com/photo-1552058544-f2b08422138a", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8f0c6e1..46c438f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -99,6 +99,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -919,6 +920,7 @@ "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", @@ -985,6 +987,7 @@ "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", @@ -1000,7 +1003,8 @@ "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" + "license": "Apache-2.0", + "peer": true }, "node_modules/@firebase/auth-compat": { "version": "0.5.28", @@ -1457,6 +1461,7 @@ "integrity": "sha512-zGlBn/9Dnya5ta9bX/fgEoNC3Cp8s6h+uYPYaDieZsFOAdHP/ExzQ/eaDgxD3GOROdPkLKpvKY0iIzr9adle0w==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" }, @@ -2168,8 +2173,7 @@ "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", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2587,6 +2591,7 @@ "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" @@ -2597,6 +2602,7 @@ "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" } @@ -2670,6 +2676,7 @@ "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", @@ -3061,6 +3068,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3476,6 +3484,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4138,6 +4147,7 @@ "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" } @@ -4728,6 +4738,7 @@ "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", @@ -6307,6 +6318,7 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "devOptional": true, "license": "MIT", + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -6530,7 +6542,6 @@ "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" } @@ -7064,6 +7075,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -7411,6 +7423,7 @@ "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" }, @@ -7423,6 +7436,7 @@ "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" @@ -7452,6 +7466,7 @@ "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" @@ -7598,7 +7613,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -8473,6 +8489,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8661,6 +8678,7 @@ "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" @@ -8802,6 +8820,7 @@ "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", @@ -8915,6 +8934,7 @@ "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/src/App.tsx b/frontend/src/App.tsx index b546b12..bb9e509 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,7 +8,9 @@ import SavedApplications from "./pages/SavedApplications"; import Companies from "./pages/Companies"; import CompanyProfile from "./pages/Company"; import AlumniProfile from "./pages/Alumni"; +import StudentProfile from "./pages/Student"; import Connect from "./pages/Connect"; +import Students from "./pages/Students"; import Sandbox from "./pages/Sandbox"; import Profile from "./pages/Profile"; import Analytics from "./pages/Analytics"; @@ -24,7 +26,9 @@ function App() { } /> } /> } /> + } /> } /> + } /> } /> } /> diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index cd3aaaf..9737606 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -3,6 +3,7 @@ import { Alumni, CreateUserRequest, GetAlumniQuery, + GetStudentsQuery, Student, UpdateUserRequest, User, @@ -21,6 +22,7 @@ function parseUser(user: UserJSON): User { throw new Error("Invalid user type"); } +// Use for when type explicitly calls for Alumni function parseAlumni(user: UserJSON): Alumni { if (user.type === UserType.Alumni) { return { ...user } as Alumni; // Safely return as Alumni @@ -28,6 +30,14 @@ function parseAlumni(user: UserJSON): Alumni { throw new Error("User is not an Alumni"); } +// Use for when type explicitly calls for Student +function parseStudent(user: UserJSON): Student { + if (user.type === UserType.Student) { + return { ...user } as Student; // Safely return as Student + } + throw new Error("User is not an Student"); +} + /** * Fetch all users from the backend. * @@ -76,6 +86,22 @@ export async function getAlumniById(id: string): Promise> { } } +/** + * Fetch a single alumni by ID from the backend. + * + * @param id The ID of the alumni to fetch + * @returns The alumni object + */ +export async function getStudentById(id: string): Promise> { + try { + const response = await get(`/api/users/${id}`); + const json = (await response.json()) as UserJSON; + return { success: true, data: parseStudent(json) }; + } catch (error) { + return handleAPIError(error); + } +} + /** * Create a new user in the backend. * @@ -154,6 +180,29 @@ export async function getAlumni( } +/** + * Fetch students that are willing to share profile from the backend + * + * @param queries + * @returns PaginatedData object containing student profiles + */ +export async function getStudents( + queries: GetStudentsQuery = { page: 0, perPage: 10 }, +): Promise>> { + try { + const response = await get(`/api/users/student`, { + ...queries, + major: queries.major?.join(",") || "", + }); + const json = (await response.json()) as PaginatedData; + const result = { ...json, data: json.data.map(parseStudent) }; + return { success: true, data: result }; + } catch (error) { + return handleAPIError(error); + } +} + + /** * Fetch similarities between a student and an alumni from the backend * @@ -161,7 +210,7 @@ export async function getAlumni( * @returns SimilarityResponse containing similarities and summary */ export async function getSimilarities( - studentId: string, + studentId: string, alumniId: string, ): Promise> { try { diff --git a/frontend/src/components/connect/StudentProfileModal.tsx b/frontend/src/components/connect/StudentProfileModal.tsx new file mode 100644 index 0000000..963e5a2 --- /dev/null +++ b/frontend/src/components/connect/StudentProfileModal.tsx @@ -0,0 +1,154 @@ +import React from "react"; +import Modal from "../public/Modal"; +import { Student } from "../../types/User"; +import { + LuMail, + LuGraduationCap, + LuPhone, +} from "react-icons/lu"; +import { BsLinkedin } from "react-icons/bs"; + +interface StudentProfileModalProps { + isOpen: boolean; + onClose: () => void; + student: Student | null; +} + +const StudentProfileModal = ({ + isOpen, + onClose, + student, +}: StudentProfileModalProps) => { + // Function to format LinkedIn URL for display + const formatLinkedInUrl = (url: string) => { + try { + const linkedInUrl = new URL(url); + // Display just the path without https://linkedin.com + return linkedInUrl.pathname.replace(/^\//, ""); + } catch { + return url; + } + }; + + if (!student) { + return
Error: Student not found
; + } + + return ( + + {/* Header with background color and profile image */} +
+ + +
+
+ {student.profilePicture ? ( + {`${student.name}'s + ) : ( +
+ + {student.name.charAt(0)} + +
+ )} +
+ +
+

{student.name}

+
+ + {student.major || "Major not specified"} +
+
+ {student.classLevel || "Class Level not specified"} +
+
+
+
+ + {/* Content */} +
+
+ {/* Contact Information */} +
+

+ Contact Information +

+ +
+ + +
+ +
+ +
+
Phone
+
{student.phoneNumber || "Not specified"}
+
+
+ + +
+
+
+
+ ); +}; + +export default StudentProfileModal; diff --git a/frontend/src/components/connect/StudentTile.tsx b/frontend/src/components/connect/StudentTile.tsx new file mode 100644 index 0000000..d59f5ca --- /dev/null +++ b/frontend/src/components/connect/StudentTile.tsx @@ -0,0 +1,98 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Student } from "../../types/User"; +import { LuMail, LuGraduationCap } from "react-icons/lu"; +import StudentProfileModal from "./StudentProfileModal"; + +interface StudentTileProps { + data: Student; +} + +const StudentTile: React.FC = ({ data }) => { + const navigate = useNavigate(); + const [studentProfileOpen, setStudentProfileOpen] = useState(false); + + const onStudentClicked = () => { + setStudentProfileOpen(true); + }; + + const onStudentProfileClose = () => { + setStudentProfileOpen(false); + }; + + return ( +
navigate(`/student/${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 */} +
+
+ {data.profilePicture ? ( + {data.name} + ) : ( + {data.name.charAt(0)} + )} +
+
+

{data.name}

+
+
+ + {/* Card body with user details */} +
+ + +
+ +
+ {data.major ? ( + data.major + ) : ( + Major not specified + )} +
+
+ +
+
+ {data.classLevel ? ( + data.classLevel.charAt(0).toUpperCase() + + data.classLevel.slice(1).toLowerCase() + ) : ( + Class Level not specified + )} +
+
+ +
+ +
+
+ +
+ ); +}; + +export default StudentTile; \ No newline at end of file diff --git a/frontend/src/components/public/DataList.tsx b/frontend/src/components/public/DataList.tsx index 256f35f..48a3b99 100644 --- a/frontend/src/components/public/DataList.tsx +++ b/frontend/src/components/public/DataList.tsx @@ -119,8 +119,8 @@ const DataList = (props: DataListProps) => { :


-

No alumni currently fit your query.

-

Edit your Industry type to find alumni in a different category.

+

No users currently fit your query.

+

Edit your filters to find users in a different category.

} diff --git a/frontend/src/constants/navItems.ts b/frontend/src/constants/navItems.ts index 740acfb..00a1093 100644 --- a/frontend/src/constants/navItems.ts +++ b/frontend/src/constants/navItems.ts @@ -27,6 +27,11 @@ export const navItems: NavItem[] = [ path: "/connect", icon: FiLink, }, + { + label: "Student Network", + path: "/students", + icon: FiLink, + }, { label: "Applications", path: "/applications/applied", diff --git a/frontend/src/pages/Student.tsx b/frontend/src/pages/Student.tsx new file mode 100644 index 0000000..59fcd9e --- /dev/null +++ b/frontend/src/pages/Student.tsx @@ -0,0 +1,540 @@ +import React, { useCallback, useEffect, useState, useRef } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { + FaArrowLeft, + FaLinkedin +} from "react-icons/fa"; +import { LuMail, LuWand, LuGraduationCap } from "react-icons/lu"; +import { FiPhone } from "react-icons/fi"; +import { FaRegCopy } from "react-icons/fa"; +import { generateEmail } from "../api/email"; +import { useAuth } from "../contexts/useAuth"; +import Modal from "../components/public/Modal"; +import { getStudentById, getSimilarities } from "../api/users"; +import { APIResult } from "../api/requests"; +import { Student } from "../types/User"; +import { ProgressSpinner } from "primereact/progressspinner"; +import { Similarity } from "../types/Similarity"; +import { Toast } from "primereact/toast"; + +const StudentProfile: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const toast = useRef(null); + console.log("route param id: ", id); + const navigate = useNavigate(); + + const [student, setStudent] = useState(null); + const [loading, setLoading] = useState(true); + const { user } = useAuth(); + + const [similarities, setSimilarities] = useState(null); + const [similaritySummary, setSimilaritySummary] = useState(null); + const [similarityLoading, setSimilarityLoading] = useState(false); + + // Email Generation State + const [isEmailModalOpen, setIsEmailModalOpen] = useState(false); + const [tone, setTone] = useState("Professional"); + const [purpose, setPurpose] = useState(""); + const [generatedEmail, setGeneratedEmail] = useState(""); + const [sharedInterests, setSharedInterests] = useState([]); + const [generating, setGenerating] = useState(false); + + const handleGenerate = async () => { + if (!user?._id || !id) return; + setGenerating(true); + setGeneratedEmail(""); + + const result = await generateEmail({ + studentId: user._id, + alumniId: id, // In this case, another student + tone, + purpose + }); + + if (result.success) { + setGeneratedEmail(result.data.email); + setSharedInterests(result.data.sharedInterests); + } else { + toast.current?.show({ + severity: "error", + summary: "Error", + detail: result.error || "Failed to generate email" + }); + } + setGenerating(false); + }; + + const copyToClipboard = () => { + navigator.clipboard.writeText(generatedEmail); + toast.current?.show({ + severity: "success", + summary: "Copied", + detail: "Email text copied to clipboard" + }); + }; + + const handleStudentUpdate = useCallback(() => { + if (!id) return; + setLoading(true); + getStudentById(id) + .then((result: APIResult) => { + if (result.success) { + setStudent(result.data); + } else { + toast.current?.clear(); + toast.current?.show({ + severity: "error", + summary: "Error", + detail: "Failed to fetch student profile: " + result.error, + }); + } + }) + .catch(() => toast.current?.show({ + severity: "error", + summary: "Error", + detail: "Unexpected error occurred.", + })) + .finally(() => setLoading(false)); + }, [id]); + + // Initial company fetch + useEffect(() => { + handleStudentUpdate(); + }, [handleStudentUpdate]); + + const resolveUserId = (): string | null => { + if (user) { + return user._id ?? null; + } + return null; + }; + + const fetchSimilarities = async () => { + if (!id) return; + + const userId = resolveUserId(); + if (!userId) { + return; + } + + try { + setSimilarityLoading(true); + const res = await getSimilarities(userId, id); + + if (!res.success) { + throw new Error("Failed to fetch similarities"); + } + + setSimilarities(res.data.similarities); + setSimilaritySummary(res.data.summary); + } finally { + setSimilarityLoading(false); + } + }; + + + if (!student) + return ( +
+ Student not found. +
+ ); + + return ( +
+ {!student ? ( +
+ Student not found. +
+ ) : ( + <> + {/* Display Spinner While Loading */} + {loading && ( +
+ +
+ )} + {/* When Finished Loading */} + {!loading && ( +
+
+ {/* Back Button to Exit Profile */} + +
+
+
+
+ {/* Left Column - Profile Image */} +
+
+ {student.profilePicture ? ( + {student.name} + ) : ( +
+ {student.name + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase()} +
+ )} +
+
+ + {/* Right Column - User Information */} +
+ {/* Basic Information */} +
+
+

+ Basic Information +

+ +
+
+
+ +

{student.name}

+
+
+ +

{student.email}

+
+ +
+ +

+ {student.phoneNumber || "Not provided"} +

+
+
+
+ + {/* LinkedIn */} +
+ + {student.linkedIn ? ( + + {student.linkedIn} + + ) : ( +

Not provided

+ )} +
+ + + {/* Student-specific Information */} +
+
+

+ Academic Information +

+
+
+ +

+ {student.major || "Not specified"} +

+
+ +
+ +

+ {student.classLevel + ? student.classLevel.charAt(0).toUpperCase() + + student.classLevel.slice(1).toLowerCase() + : "Not specified"} +

+
+ + {/* School */} +
+ +

+ {student.school + ? student.school.charAt(0).toUpperCase() + + student.school.slice(1).toLowerCase() + : "Not specified"} +

+
+ {/* Field of Interest */} +
+ + {Array.isArray(student.fieldOfInterest) && student.fieldOfInterest.length > 0 ? ( +
+ {student.fieldOfInterest.map((field, index) => ( + + {field} + + ))} +
+ ) : ( +

Not specified

+ )} +
+ {/* Projects */} +
+ + {Array.isArray(student.projects) && student.projects.length > 0 ? ( +
+ {student.projects.map((project, index) => ( + + {project} + + ))} +
+ ) : ( +

Not specified

+ )} +
+ {/* These next two are technically user specific but this will be dealt with during the redesign + Hobbies */} +
+ + {Array.isArray(student.hobbies) && student.hobbies.length > 0 ? ( +
+ {student.hobbies.map((hobby, index) => ( + + {hobby} + + ))} +
+ ) : ( +

Not specified

+ )} +
+ {/* Skills */} +
+ + {Array.isArray(student.skills) && student.skills.length > 0 ? ( +
+ {student.skills.map((skill, index) => ( + + {skill} + + ))} +
+ ) : ( +

Not specified

+ )} +
+ {/* Companies of Interest */} +
+ + {Array.isArray(student.companiesOfInterest) && student.companiesOfInterest.length > 0 ? ( +
+ {student.companiesOfInterest.map((companiesOfInterest, index) => ( + + {companiesOfInterest} + + ))} +
+ ) : ( +

Not specified

+ )} +
+
+
+ + {/* Similarities section */} +
+ + + {similarities && ( +
+

+ Similarities +

+
    + {similarities.map((sim, idx) => ( +
  • + {sim.category}:{" "} + {sim.description} +
  • + ))} +
+ + {similaritySummary && ( +

{similaritySummary}

+ )} +
+ )} +
+
+
+ + +
+
+
+
+ )} + + )} + + {/* Email Generation Modal */} + setIsEmailModalOpen(false)} + className="w-full max-w-2xl p-6 rounded-xl" + useOverlay + > +
+

+ + Personalize Email +

+

+ Generate a personalized outreach email to {student?.name} based on your shared interests. +

+ +
+
+ + +
+
+ + setPurpose(e.target.value)} + placeholder="e.g. Ask for resume advice" + className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none" + /> +
+
+ + + + {generatedEmail && ( +
+
+ Generated Draft + +
+