From 5a4f57fce3fef54fbed9697bd37ba1c7e11b405a Mon Sep 17 00:00:00 2001 From: ColumnSkunky Date: Thu, 17 Apr 2025 18:39:57 -0700 Subject: [PATCH 1/9] feat: added the match score algorithm --- frontend/bitmatch/src/views/MatchScore.js | 82 +++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 frontend/bitmatch/src/views/MatchScore.js diff --git a/frontend/bitmatch/src/views/MatchScore.js b/frontend/bitmatch/src/views/MatchScore.js new file mode 100644 index 0000000..e01a2b8 --- /dev/null +++ b/frontend/bitmatch/src/views/MatchScore.js @@ -0,0 +1,82 @@ +let projects = fetch('http://localhost:5432/', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => console.log(data)) + .catch(error => console.error('Error fetching projects:', error)); + +let user = fetch('http://localhost:8000/user_123/') + .then(res => res.json()) + .then(data => console.log('User data:', data)) + .catch(error => console.error('Error fetching user:', error)); + +function calculateMatchScore(Project, User) { + // Define weights for each attribute (optional) + const weights = { + school: 0.25, + jobTitle: 0.15, + tags: 0.60 + }; + + // Function to compare two values and return a score (0 to 1) + function compareValues(valueA, valueB) { + if (valueA === valueB) { + return 1; // Perfect match + } else { + // For non-numeric values, return 0 if they don't match + return 0; + } + } + + // Function to compare tags of Project and User + function compareTags(tagsP, tagsU){ + let count = 0; // Value to keep Track of how many instances of + let score = 0.0; + for(let i = 0; i < tagsP.length; i++){ + for(let j = 0; j < tagsP.length; j ++){ + if (tagsU[i] == tagsP[j]){ + count ++; + } + } + } + score = (count/tagsP.length); + return score; + } + // Calculate scores for each attribute + const score1 = compareValues(Project.school, User.school) + const score2 = compareValues(Project.jobTitle, User.jobTitle) + const score3 = compareTags(Project.tags, User.tags) + // Sum the scores + const totalScore = (score1 * 0.25) + (score2 * 0.15) + (score3 * 0.6); + + // Normalize the score to a range of 0 to 100 + const normalizedScore = Math.round(totalScore * 100); + return normalizedScore; +} + +function main(){ + let scores = []; + for(let i = projects; projects.length(); i++){ + scores.push(calculateMatchScore(projects[i], user)); + } + scores.sort((a,b) => a-b); +} +// Example usage +/*const Project = { + school: "Cal Poly Pomona", + jobTitle: "Data Scientist", + tags: ["Python", "Data Science"] +}; + +const User = { + school: "Cal Poly Pomona", + jobTitle: "Software Engineer", + tags: ["Python", "Object Orientated Programing"] +}; + + +let sample = calculateMatchScore(Project, User); +console.log(sample);*/ \ No newline at end of file From 93a455886bb7de9c8b85742ba893b42da8868dba Mon Sep 17 00:00:00 2001 From: Larry La Date: Fri, 18 Apr 2025 12:25:40 -0700 Subject: [PATCH 2/9] chore: pull data and pass into matchscoreutils --- frontend/bitmatch/src/views/MatchScore.js | 82 ----------- .../bitmatch/src/views/MatchScoreUtils.js | 137 ++++++++++++++++++ .../bitmatch/src/views/ProjectListPage.jsx | 46 ++++-- 3 files changed, 170 insertions(+), 95 deletions(-) delete mode 100644 frontend/bitmatch/src/views/MatchScore.js create mode 100644 frontend/bitmatch/src/views/MatchScoreUtils.js diff --git a/frontend/bitmatch/src/views/MatchScore.js b/frontend/bitmatch/src/views/MatchScore.js deleted file mode 100644 index e01a2b8..0000000 --- a/frontend/bitmatch/src/views/MatchScore.js +++ /dev/null @@ -1,82 +0,0 @@ -let projects = fetch('http://localhost:5432/', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - } - }) - .then(response => response.json()) - .then(data => console.log(data)) - .catch(error => console.error('Error fetching projects:', error)); - -let user = fetch('http://localhost:8000/user_123/') - .then(res => res.json()) - .then(data => console.log('User data:', data)) - .catch(error => console.error('Error fetching user:', error)); - -function calculateMatchScore(Project, User) { - // Define weights for each attribute (optional) - const weights = { - school: 0.25, - jobTitle: 0.15, - tags: 0.60 - }; - - // Function to compare two values and return a score (0 to 1) - function compareValues(valueA, valueB) { - if (valueA === valueB) { - return 1; // Perfect match - } else { - // For non-numeric values, return 0 if they don't match - return 0; - } - } - - // Function to compare tags of Project and User - function compareTags(tagsP, tagsU){ - let count = 0; // Value to keep Track of how many instances of - let score = 0.0; - for(let i = 0; i < tagsP.length; i++){ - for(let j = 0; j < tagsP.length; j ++){ - if (tagsU[i] == tagsP[j]){ - count ++; - } - } - } - score = (count/tagsP.length); - return score; - } - // Calculate scores for each attribute - const score1 = compareValues(Project.school, User.school) - const score2 = compareValues(Project.jobTitle, User.jobTitle) - const score3 = compareTags(Project.tags, User.tags) - // Sum the scores - const totalScore = (score1 * 0.25) + (score2 * 0.15) + (score3 * 0.6); - - // Normalize the score to a range of 0 to 100 - const normalizedScore = Math.round(totalScore * 100); - return normalizedScore; -} - -function main(){ - let scores = []; - for(let i = projects; projects.length(); i++){ - scores.push(calculateMatchScore(projects[i], user)); - } - scores.sort((a,b) => a-b); -} -// Example usage -/*const Project = { - school: "Cal Poly Pomona", - jobTitle: "Data Scientist", - tags: ["Python", "Data Science"] -}; - -const User = { - school: "Cal Poly Pomona", - jobTitle: "Software Engineer", - tags: ["Python", "Object Orientated Programing"] -}; - - -let sample = calculateMatchScore(Project, User); -console.log(sample);*/ \ No newline at end of file diff --git a/frontend/bitmatch/src/views/MatchScoreUtils.js b/frontend/bitmatch/src/views/MatchScoreUtils.js new file mode 100644 index 0000000..7489492 --- /dev/null +++ b/frontend/bitmatch/src/views/MatchScoreUtils.js @@ -0,0 +1,137 @@ +// function calculateMatchScore(Project, User) { +// // Define weights for each attribute (optional) +// const weights = { +// school: 0.25, +// jobTitle: 0.15, +// tags: 0.6, +// }; + +// // Function to compare two values and return a score (0 to 1) +// function compareValues(valueA, valueB) { +// if (valueA === valueB) { +// return 1; // Perfect match +// } else { +// // For non-numeric values, return 0 if they don't match +// return 0; +// } +// } + +// // Function to compare tags of Project and User +// function compareTags(tagsP, tagsU) { +// let count = 0; // Value to keep Track of how many instances of +// let score = 0.0; +// for (let i = 0; i < tagsP.length; i++) { +// for (let j = 0; j < tagsP.length; j++) { +// if (tagsU[i] == tagsP[j]) { +// count++; +// } +// } +// } +// score = count / tagsP.length; +// return score; +// } +// // Calculate scores for each attribute +// const score1 = compareValues(Project.school, User.school); +// const score2 = compareValues(Project.jobTitle, User.jobTitle); +// const score3 = compareTags(Project.tags, User.tags); +// // Sum the scores +// const totalScore = score1 * 0.25 + score2 * 0.15 + score3 * 0.6; + +// // Normalize the score to a range of 0 to 100 +// const normalizedScore = Math.round(totalScore * 100); +// return normalizedScore; +// } + +// function main() { +// let scores = []; +// for (let i = projects; projects.length(); i++) { +// scores.push(calculateMatchScore(projects[i], user)); +// } +// scores.sort((a, b) => a - b); +// } +// // Example usage +// /*const Project = { +// school: "Cal Poly Pomona", +// jobTitle: "Data Scientist", +// tags: ["Python", "Data Science"] +// }; + +// const User = { +// school: "Cal Poly Pomona", +// jobTitle: "Software Engineer", +// tags: ["Python", "Object Orientated Programing"] +// }; + +// let sample = calculateMatchScore(Project, User); +// console.log(sample);*/ + +// Placeholder function to calculate the match score +// TODO: MAKE THIS ACTUALLY CALCULATE CORRECTLY BASED OFF DATA, RN IT IS RANDOMLY CALCULATING BELOW ARE THE EXAMPLES OF THE DATA PASSED IN. +// PLEASE USE HELPER FUNCTIONS, ONE GIANT FUNCTION NOT IDEAL. +export const calculateMatchScores = (userData, projects) => { + console.log(userData); + + // Mock User Profile: + // { + // id: "610c6c0f-dcf2-432f-b64b-9ec94481ef8d", + // auth_id: "user_2tTc9slPZTzbFvJWsujHu4MMg62", + // first_name: "Larry", + // last_name: "La", + // username: "larrylaa", + // email: "larryquocla@gmail.com", + // about_me: "As a rising junior computer science student at California State Polytechnic University-Pomona, I am passionate about learning and applying new technologies to create impactful software.", + // college: "Cal Poly Pomona", + // major: "Computer Science", + // grad_date: "May 2027", + // location: "El Monte, CA", + // interests: ["Backend", "Cloud Computing", "DevOps", "Frontend"], + // roles: ["Backend Web Developer", "Cloud Engineer", "Cloud Security Engineer", "DevOps Engineer", "Software Engineer"], + // skills: ["AWS", "Azure", "Back-End Web Development", "C#", "ExpressJS", "Git/Github", "HTML/CSS", "Java", "JavaScript", "Postgres", "Powershell", "Python", "React", "SQL", "Terraform", "VBA"], + // location_preferences: ["Near my University or College location", "Remote friendly projects", "Near my location"], + // followers: 0, + // likes: 0, + // owned: [], + // projects: ["67e6f820-b053-48d8-a7dd-db11396d3310", "37bfe931-7be0-42b1-ab85-abd110c22ff7"] + // } + + console.log(projects); + + // Mock Project: + // { + // id: "4cca4a32-ba4b-4ad7-ab4b-e298d428ef5a", + // title: "Mobile Pirate Game", + // description: "This is a project to make a mobile side scrolling pirate game.", + // full_description: "This is a project to make a mobile side scrolling pirate game.", + // image_url: "https://bitmatch-django-media-bucket.s3.amazonaws.com/projects/home_bg_landscape.png", + // institution: "California Polytechnic State University Pomona", + // owner: "db164a04-b296-41f8-b70d-ef31cda89f3e", + // followers_count: 0, + // likes_count: 0, + // positions: [ + // // Example structure: + // // { + // // title: "Frontend Developer", + // // skills_required: ["React", "JavaScript"], + // // filled: false + // // } + // ], + // location: [], + // interest_tags: null, + // skill_tags: null, + // group: "", + // email: null, + // other_contact: null, + // updates: null, + // wanted_description: null, + // } + + return projects.map((project) => { + // Placeholder logic: randomly assign a score between 1 and 100 + const match_percentage = Math.floor(Math.random() * 100) + 1; + + return { + ...project, + match_percentage: match_percentage, + }; + }); +}; diff --git a/frontend/bitmatch/src/views/ProjectListPage.jsx b/frontend/bitmatch/src/views/ProjectListPage.jsx index ff62909..06384a2 100644 --- a/frontend/bitmatch/src/views/ProjectListPage.jsx +++ b/frontend/bitmatch/src/views/ProjectListPage.jsx @@ -1,12 +1,12 @@ import { useEffect, useState } from "react"; -import { ChevronRight, Plus } from "lucide-react"; +import { Plus } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { Link } from "react-router-dom"; import ProjectCardLarge from "@/components/project/ProjectCardLarge"; const SERVER_HOST = import.meta.env.VITE_SERVER_HOST; import axios from "axios"; import { useNavigate } from "react-router-dom"; import { useUser } from "@clerk/clerk-react"; +import { calculateMatchScores } from "./MatchScoreUtils"; export default function ProjectListPage() { const navigate = useNavigate(); @@ -46,22 +46,42 @@ export default function ProjectListPage() { const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + useEffect(() => { - axios - .get(`${SERVER_HOST}/projects/`) - .then((response) => { - const sortedProjects = response.data.sort( + const fetchUserAndProjects = async () => { + try { + // Fetch user data + const userResponse = await fetch(`${SERVER_HOST}/userauth/${user.id}`); + if (!userResponse.ok) { + throw new Error("Failed to fetch user data"); + } + const userData = await userResponse.json(); + + // Fetch project data + const projectsResponse = await axios.get(`${SERVER_HOST}/projects/`); + const projects = projectsResponse.data; + + // Pass both userData and projects to a function for match score calculation + const projectsWithScores = calculateMatchScores(userData, projects); + + // Sort projects based on match score + const sortedProjects = projectsWithScores.sort( (a, b) => b.match_percentage - a.match_percentage ); + + // Update state with sorted projects setProjects(sortedProjects); + console.log(sortedProjects); + } catch (error) { + console.error("Error fetching user and projects:", error); + setError("Couldn't load projects or user data."); + } finally { setLoading(false); - }) - .catch((error) => { - setError("Couldn't load projects."); - console.error("Error fetching projects:", error); - setLoading(false); - }); - }, []); + } + }; + + fetchUserAndProjects(); + }, [user.id]); if (loading) { return ( From 149a767a44917f2daa0f5e94e373092b3d63a8b4 Mon Sep 17 00:00:00 2001 From: Larry La Date: Fri, 18 Apr 2025 12:30:08 -0700 Subject: [PATCH 3/9] chore: add for home page also --- frontend/bitmatch/src/views/HomePage.jsx | 44 +++++++++++++++++------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/frontend/bitmatch/src/views/HomePage.jsx b/frontend/bitmatch/src/views/HomePage.jsx index ffc3673..678bc9d 100644 --- a/frontend/bitmatch/src/views/HomePage.jsx +++ b/frontend/bitmatch/src/views/HomePage.jsx @@ -4,6 +4,7 @@ import ImageSlideshow from "../components/ui/ImageSlideshow"; import ProjectCarousel from "../components/ui/ProjectCarousel"; import { useNavigate } from "react-router-dom"; import { useUser } from "@clerk/clerk-react"; +import { calculateMatchScores } from "./MatchScoreUtils"; const SERVER_HOST = import.meta.env.VITE_SERVER_HOST; @@ -55,23 +56,40 @@ export default function Home() { // Fetch project data on component mount useEffect(() => { - axios - .get(`${SERVER_HOST}/projects/`) - .then((response) => { - const sortedProjects = response.data.sort( + const fetchUserAndProjects = async () => { + try { + // Fetch user data + const userResponse = await fetch(`${SERVER_HOST}/userauth/${user.id}`); + if (!userResponse.ok) { + throw new Error("Failed to fetch user data"); + } + const userData = await userResponse.json(); + + // Fetch project data + const projectsResponse = await axios.get(`${SERVER_HOST}/projects/`); + const projects = projectsResponse.data; + + // Pass both userData and projects to a function for match score calculation + const projectsWithScores = calculateMatchScores(userData, projects); + + // Sort projects based on match score + const sortedProjects = projectsWithScores.sort( (a, b) => b.match_percentage - a.match_percentage ); + + // Update state with sorted projects setProjects(sortedProjects); + console.log(sortedProjects); + } catch (error) { + console.error("Error fetching user and projects:", error); + setError("Couldn't load projects or user data."); + } finally { setLoading(false); - }) - .catch((error) => { - console.error("Error fetching projects:", error); - setError( - "Couldn't load projects. (DEV MESSAGE: Ensure the backend server is running)" - ); - setLoading(false); - }); - }, []); + } + }; + + fetchUserAndProjects(); + }, [user.id]); // Loading state if (loading) { From c431335baa129823698250f0a6dc75974e56d3b2 Mon Sep 17 00:00:00 2001 From: Larry La Date: Fri, 18 Apr 2025 12:35:31 -0700 Subject: [PATCH 4/9] chore: abstract user error msg --- frontend/bitmatch/src/views/HomePage.jsx | 2 +- frontend/bitmatch/src/views/ProjectListPage.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/bitmatch/src/views/HomePage.jsx b/frontend/bitmatch/src/views/HomePage.jsx index 678bc9d..3cd1f4c 100644 --- a/frontend/bitmatch/src/views/HomePage.jsx +++ b/frontend/bitmatch/src/views/HomePage.jsx @@ -82,7 +82,7 @@ export default function Home() { console.log(sortedProjects); } catch (error) { console.error("Error fetching user and projects:", error); - setError("Couldn't load projects or user data."); + setError("Something went wrong. Please try again later."); } finally { setLoading(false); } diff --git a/frontend/bitmatch/src/views/ProjectListPage.jsx b/frontend/bitmatch/src/views/ProjectListPage.jsx index 06384a2..1f320e7 100644 --- a/frontend/bitmatch/src/views/ProjectListPage.jsx +++ b/frontend/bitmatch/src/views/ProjectListPage.jsx @@ -74,7 +74,7 @@ export default function ProjectListPage() { console.log(sortedProjects); } catch (error) { console.error("Error fetching user and projects:", error); - setError("Couldn't load projects or user data."); + setError("Something went wrong. Please try again later."); } finally { setLoading(false); } From c882e2d168f2acf88edd426fc76bc4c1b232630a Mon Sep 17 00:00:00 2001 From: ColumnSkunky Date: Sat, 19 Apr 2025 19:51:26 -0700 Subject: [PATCH 5/9] fix: MatchScoreUtils.js to calculate new user values as well as correctly outputs weighted scores --- .../bitmatch/src/views/MatchScoreUtils.js | 68 +++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/frontend/bitmatch/src/views/MatchScoreUtils.js b/frontend/bitmatch/src/views/MatchScoreUtils.js index 7489492..247bb14 100644 --- a/frontend/bitmatch/src/views/MatchScoreUtils.js +++ b/frontend/bitmatch/src/views/MatchScoreUtils.js @@ -6,6 +6,8 @@ // tags: 0.6, // }; +import ProjectCardLarge from "@/components/project/ProjectCardLarge"; + // // Function to compare two values and return a score (0 to 1) // function compareValues(valueA, valueB) { // if (valueA === valueB) { @@ -64,12 +66,52 @@ // let sample = calculateMatchScore(Project, User); // console.log(sample);*/ +export function compareTags (userTags, projectTags) { + /*const sortedUserTags = userTags.sort(); + const sortedProjectTags = projectTags.sort(); + const count = 0; + for(let i = 0; i < sortedProjectTags.length; i ++){ + if (sortedUserTags[i] == sortedProjectTags[i]){ + count ++ + } + }*/ + + const userSet = new Set(userTags); + const projectSet = new Set(projectTags); + let matches = 0; + for(let tags of projectSet){ + if(userSet.has(tags)){ + matches ++; + } + } + + return matches / projectSet.size; +} + +export function compareValues (userAtt, projectAtt){ + if(userAtt == projectAtt){ + return 1; + } + else{ + return 0; + } +} +export function roleSearch(userRole, projectPosition){ + const userSet = new Set(userRole); + const projectSet = new Set(projectPosition); + for (let role of projectSet){ + if (userSet.has(role)){ + return 1; + } + } + return 0; +} // Placeholder function to calculate the match score // TODO: MAKE THIS ACTUALLY CALCULATE CORRECTLY BASED OFF DATA, RN IT IS RANDOMLY CALCULATING BELOW ARE THE EXAMPLES OF THE DATA PASSED IN. // PLEASE USE HELPER FUNCTIONS, ONE GIANT FUNCTION NOT IDEAL. export const calculateMatchScores = (userData, projects) => { - console.log(userData); + console.log(userData.location); // Mock User Profile: // { @@ -124,11 +166,29 @@ export const calculateMatchScores = (userData, projects) => { // updates: null, // wanted_description: null, // } - + + //3 institution 15 + //5 location 5 + //4 intrest tags 10 + //1 skill tags 50 + //2 positions/roles 20 + + return projects.map((project) => { - // Placeholder logic: randomly assign a score between 1 and 100 - const match_percentage = Math.floor(Math.random() * 100) + 1; + + + const interests = compareTags(userData.interests, project.interest_tags || []); + const skills = compareTags(userData.skills, project.skill_tags || []); + const school = compareValues(userData.college, project.institution); + const location = compareValues(userData.location, project.location?.[0] || ''); + const role = roleSearch(userData.roles, project.positions); + + const weightedScore = (interests * 0.1) + (skills * 0.5) + (school * 0.15) + (location * 0.05) + (role * 0.2); + + //Creates a match percentage and rounds the value + const match_percentage = Math.round(weightedScore * 100); + return { ...project, match_percentage: match_percentage, From edae24ef28a35e59a96db64815cf1c24fdf53926 Mon Sep 17 00:00:00 2001 From: Larry La Date: Sat, 19 Apr 2025 20:13:08 -0700 Subject: [PATCH 6/9] feat: new calculation logic --- .../bitmatch/src/views/MatchScoreUtils.js | 230 ++++-------------- 1 file changed, 49 insertions(+), 181 deletions(-) diff --git a/frontend/bitmatch/src/views/MatchScoreUtils.js b/frontend/bitmatch/src/views/MatchScoreUtils.js index 247bb14..d108bde 100644 --- a/frontend/bitmatch/src/views/MatchScoreUtils.js +++ b/frontend/bitmatch/src/views/MatchScoreUtils.js @@ -1,197 +1,65 @@ -// function calculateMatchScore(Project, User) { -// // Define weights for each attribute (optional) -// const weights = { -// school: 0.25, -// jobTitle: 0.15, -// tags: 0.6, -// }; - -import ProjectCardLarge from "@/components/project/ProjectCardLarge"; - -// // Function to compare two values and return a score (0 to 1) -// function compareValues(valueA, valueB) { -// if (valueA === valueB) { -// return 1; // Perfect match -// } else { -// // For non-numeric values, return 0 if they don't match -// return 0; -// } -// } - -// // Function to compare tags of Project and User -// function compareTags(tagsP, tagsU) { -// let count = 0; // Value to keep Track of how many instances of -// let score = 0.0; -// for (let i = 0; i < tagsP.length; i++) { -// for (let j = 0; j < tagsP.length; j++) { -// if (tagsU[i] == tagsP[j]) { -// count++; -// } -// } -// } -// score = count / tagsP.length; -// return score; -// } -// // Calculate scores for each attribute -// const score1 = compareValues(Project.school, User.school); -// const score2 = compareValues(Project.jobTitle, User.jobTitle); -// const score3 = compareTags(Project.tags, User.tags); -// // Sum the scores -// const totalScore = score1 * 0.25 + score2 * 0.15 + score3 * 0.6; - -// // Normalize the score to a range of 0 to 100 -// const normalizedScore = Math.round(totalScore * 100); -// return normalizedScore; -// } - -// function main() { -// let scores = []; -// for (let i = projects; projects.length(); i++) { -// scores.push(calculateMatchScore(projects[i], user)); -// } -// scores.sort((a, b) => a - b); -// } -// // Example usage -// /*const Project = { -// school: "Cal Poly Pomona", -// jobTitle: "Data Scientist", -// tags: ["Python", "Data Science"] -// }; +// Helper: Compare simple values (school, jobTitle) +function compareValues(valueA, valueB) { + return valueA === valueB ? 1 : 0; +} -// const User = { -// school: "Cal Poly Pomona", -// jobTitle: "Software Engineer", -// tags: ["Python", "Object Orientated Programing"] -// }; +// Helper: Compare tags (intersection similarity) +function compareTags(tagsA, tagsB) { + if (!tagsA || !tagsB || !Array.isArray(tagsA) || !Array.isArray(tagsB)) + return 0; -// let sample = calculateMatchScore(Project, User); -// console.log(sample);*/ -export function compareTags (userTags, projectTags) { - /*const sortedUserTags = userTags.sort(); - const sortedProjectTags = projectTags.sort(); - const count = 0; - for(let i = 0; i < sortedProjectTags.length; i ++){ - if (sortedUserTags[i] == sortedProjectTags[i]){ - count ++ - } - }*/ + const setA = new Set(tagsA.map((tag) => tag.toLowerCase())); + const setB = new Set(tagsB.map((tag) => tag.toLowerCase())); - const userSet = new Set(userTags); - const projectSet = new Set(projectTags); - let matches = 0; - for(let tags of projectSet){ - if(userSet.has(tags)){ - matches ++; - } - } - - return matches / projectSet.size; + const intersection = [...setA].filter((tag) => setB.has(tag)); + return intersection.length / Math.max(setA.size, 1); // Avoid division by zero } -export function compareValues (userAtt, projectAtt){ - if(userAtt == projectAtt){ - return 1; - } - else{ - return 0; +// Main: Calculate match score for one project and one user +function calculateMatchScore(project, user) { + // Weights + const weights = { + school: 0.25, + jobTitle: 0.15, + tags: 0.6, + }; + + const schoolScore = compareValues( + project.institution?.toLowerCase(), + user.college?.toLowerCase() + ); + + // Compare against ALL project positions and get best matching role/title + let jobScore = 0; + if (project.positions?.length) { + jobScore = Math.max( + ...project.positions.map((pos) => + user.roles.includes(pos.title) ? 1 : 0 + ) + ); } -} -export function roleSearch(userRole, projectPosition){ - const userSet = new Set(userRole); - const projectSet = new Set(projectPosition); - for (let role of projectSet){ - if (userSet.has(role)){ - return 1; - } - } - return 0; -} -// Placeholder function to calculate the match score -// TODO: MAKE THIS ACTUALLY CALCULATE CORRECTLY BASED OFF DATA, RN IT IS RANDOMLY CALCULATING BELOW ARE THE EXAMPLES OF THE DATA PASSED IN. -// PLEASE USE HELPER FUNCTIONS, ONE GIANT FUNCTION NOT IDEAL. -export const calculateMatchScores = (userData, projects) => { - console.log(userData.location); + // Compare tags: interest_tags and skill_tags + const interestScore = compareTags(project.interest_tags, user.interests); + const skillScore = compareTags(project.skill_tags, user.skills); + const tagScore = (interestScore + skillScore) / 2; - // Mock User Profile: - // { - // id: "610c6c0f-dcf2-432f-b64b-9ec94481ef8d", - // auth_id: "user_2tTc9slPZTzbFvJWsujHu4MMg62", - // first_name: "Larry", - // last_name: "La", - // username: "larrylaa", - // email: "larryquocla@gmail.com", - // about_me: "As a rising junior computer science student at California State Polytechnic University-Pomona, I am passionate about learning and applying new technologies to create impactful software.", - // college: "Cal Poly Pomona", - // major: "Computer Science", - // grad_date: "May 2027", - // location: "El Monte, CA", - // interests: ["Backend", "Cloud Computing", "DevOps", "Frontend"], - // roles: ["Backend Web Developer", "Cloud Engineer", "Cloud Security Engineer", "DevOps Engineer", "Software Engineer"], - // skills: ["AWS", "Azure", "Back-End Web Development", "C#", "ExpressJS", "Git/Github", "HTML/CSS", "Java", "JavaScript", "Postgres", "Powershell", "Python", "React", "SQL", "Terraform", "VBA"], - // location_preferences: ["Near my University or College location", "Remote friendly projects", "Near my location"], - // followers: 0, - // likes: 0, - // owned: [], - // projects: ["67e6f820-b053-48d8-a7dd-db11396d3310", "37bfe931-7be0-42b1-ab85-abd110c22ff7"] - // } + // Total weighted score + const totalScore = + schoolScore * weights.school + + jobScore * weights.jobTitle + + tagScore * weights.tags; - console.log(projects); + return Math.round(totalScore * 100); // Normalize to 0–100 +} - // Mock Project: - // { - // id: "4cca4a32-ba4b-4ad7-ab4b-e298d428ef5a", - // title: "Mobile Pirate Game", - // description: "This is a project to make a mobile side scrolling pirate game.", - // full_description: "This is a project to make a mobile side scrolling pirate game.", - // image_url: "https://bitmatch-django-media-bucket.s3.amazonaws.com/projects/home_bg_landscape.png", - // institution: "California Polytechnic State University Pomona", - // owner: "db164a04-b296-41f8-b70d-ef31cda89f3e", - // followers_count: 0, - // likes_count: 0, - // positions: [ - // // Example structure: - // // { - // // title: "Frontend Developer", - // // skills_required: ["React", "JavaScript"], - // // filled: false - // // } - // ], - // location: [], - // interest_tags: null, - // skill_tags: null, - // group: "", - // email: null, - // other_contact: null, - // updates: null, - // wanted_description: null, - // } - - //3 institution 15 - //5 location 5 - //4 intrest tags 10 - //1 skill tags 50 - //2 positions/roles 20 - - +// Entry Point: Calculate for all projects +export const calculateMatchScores = (userData, projects) => { return projects.map((project) => { - - - const interests = compareTags(userData.interests, project.interest_tags || []); - const skills = compareTags(userData.skills, project.skill_tags || []); - const school = compareValues(userData.college, project.institution); - const location = compareValues(userData.location, project.location?.[0] || ''); - const role = roleSearch(userData.roles, project.positions); - - const weightedScore = (interests * 0.1) + (skills * 0.5) + (school * 0.15) + (location * 0.05) + (role * 0.2); - - - //Creates a match percentage and rounds the value - const match_percentage = Math.round(weightedScore * 100); - + const match_percentage = calculateMatchScore(project, userData); return { ...project, - match_percentage: match_percentage, + match_percentage, }; }); }; From 009aff4ceb3cfe07f3aa093884560affabbcd87a Mon Sep 17 00:00:00 2001 From: Larry La Date: Sat, 19 Apr 2025 20:59:54 -0700 Subject: [PATCH 7/9] chore: make tags be added on project creation --- .../bitmatch/src/views/AddProjectPage.jsx | 138 +++++++++++++++++- .../src/views/IndividualProjectPage.jsx | 33 ++++- 2 files changed, 168 insertions(+), 3 deletions(-) diff --git a/frontend/bitmatch/src/views/AddProjectPage.jsx b/frontend/bitmatch/src/views/AddProjectPage.jsx index 7250248..995ad9e 100644 --- a/frontend/bitmatch/src/views/AddProjectPage.jsx +++ b/frontend/bitmatch/src/views/AddProjectPage.jsx @@ -12,6 +12,12 @@ const CREATE_PROJECT_ENDPOINT = `${SERVER_HOST}/projects/create/`; export default function CreateProjectForm() { const navigate = useNavigate(); const { user } = useUser(); + const [interests, setInterests] = useState([]); + const [newInterest, setNewInterest] = useState(""); + const [interestError, setInterestError] = useState(""); + const [skills, setSkills] = useState([]); + const [newSkill, setNewSkill] = useState(""); + const [skillError, setSkillError] = useState(""); const [projectName, setProjectName] = useState(""); const [university, setUniversity] = useState(""); const [group, setGroup] = useState(""); @@ -139,6 +145,13 @@ export default function CreateProjectForm() { formData.append("positions", JSON.stringify(roles)); formData.append("image_url", coverImageFile); formData.append("owner", userUuid); + skills.forEach((skill) => { + formData.append("skill_tags", skill); + }); + + interests.forEach((interest) => { + formData.append("interest_tags", interest); + }); try { const response = await fetch(CREATE_PROJECT_ENDPOINT, { @@ -185,8 +198,6 @@ export default function CreateProjectForm() { } finally { setIsLoading(false); } - - // TODO: api call to add project to user also. }; return ( @@ -406,6 +417,129 @@ export default function CreateProjectForm() { )} +
+ +
+ { + setNewInterest(e.target.value); + setInterestError(""); + }} + className="flex-grow border rounded-md p-2 focus:ring-blue-500 focus:border-blue-500" + placeholder="e.g., Backend, Frontend, DevOps, AI" + /> + +
+ {interestError && ( +

{interestError}

+ )} +
+ {interests.length === 0 ? ( +

+ No interests added yet. +

+ ) : ( + interests.map((interest, index) => ( +
+ {interest} + +
+ )) + )} +
+
+ +
+ +
+ { + setNewSkill(e.target.value); + setSkillError(""); + }} + className="flex-grow border rounded-md p-2 focus:ring-blue-500 focus:border-blue-500" + placeholder="e.g., JavaScript, Figma, Django" + /> + +
+ {skillError && ( +

{skillError}

+ )} +
+ {skills.length === 0 ? ( +

+ No skills added yet. +

+ ) : ( + skills.map((skill, index) => ( +
+ {skill} + +
+ )) + )} +
+
{error && (

diff --git a/frontend/bitmatch/src/views/IndividualProjectPage.jsx b/frontend/bitmatch/src/views/IndividualProjectPage.jsx index c48a444..2cd000d 100644 --- a/frontend/bitmatch/src/views/IndividualProjectPage.jsx +++ b/frontend/bitmatch/src/views/IndividualProjectPage.jsx @@ -10,6 +10,7 @@ import { import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { MemberCard } from "@/components/project/MemberCard"; +import { Badge } from "@/components/ui/badge"; import { PositionCard } from "@/components/project/PositionCard"; import React, { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; @@ -535,11 +536,41 @@ const ProjectDetailPage = () => {

Overview

-

+

Background & More Details About the Project

{project.full_description}

+ +
+

Project Categories

+ +
+ {project.interest_tags.map((interest, index) => ( + + {interest} + + ))} +
+ +

Desired Skills

+ +
+ {project.skill_tags.map((skill, index) => ( + + {skill} + + ))} +
+
From f37f8823c78d97c607722fe5884368c368c2992b Mon Sep 17 00:00:00 2001 From: Larry La Date: Sat, 19 Apr 2025 21:38:38 -0700 Subject: [PATCH 8/9] chore: make score sseem more natural --- .../bitmatch/src/views/MatchScoreUtils.js | 80 ++++++++++++------- 1 file changed, 53 insertions(+), 27 deletions(-) diff --git a/frontend/bitmatch/src/views/MatchScoreUtils.js b/frontend/bitmatch/src/views/MatchScoreUtils.js index d108bde..1c66514 100644 --- a/frontend/bitmatch/src/views/MatchScoreUtils.js +++ b/frontend/bitmatch/src/views/MatchScoreUtils.js @@ -1,59 +1,85 @@ -// Helper: Compare simple values (school, jobTitle) +// Compare simple string values (e.g., institution, location) function compareValues(valueA, valueB) { - return valueA === valueB ? 1 : 0; + if (!valueA || !valueB) return 0; + + const a = + typeof valueA === "string" + ? valueA.trim().toLowerCase() + : String(valueA).toLowerCase(); + const b = + typeof valueB === "string" + ? valueB.trim().toLowerCase() + : String(valueB).toLowerCase(); + + return a === b ? 1 : 0; } -// Helper: Compare tags (intersection similarity) +// Compare array-type tags (e.g., interests, skills) function compareTags(tagsA, tagsB) { - if (!tagsA || !tagsB || !Array.isArray(tagsA) || !Array.isArray(tagsB)) - return 0; + if (!Array.isArray(tagsA) || !Array.isArray(tagsB)) return 0; const setA = new Set(tagsA.map((tag) => tag.toLowerCase())); const setB = new Set(tagsB.map((tag) => tag.toLowerCase())); const intersection = [...setA].filter((tag) => setB.has(tag)); - return intersection.length / Math.max(setA.size, 1); // Avoid division by zero + return intersection.length / Math.max(setA.size, 1); // Avoid div by zero +} + +// Simple deterministic hash function +function simpleHash(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = (hash << 5) - hash + str.charCodeAt(i); + hash |= 0; // Convert to 32-bit integer + } + return hash; +} + +// Stable score variation based on project/user identity +function addStableVariation(score, projectId, userId) { + const combined = `${projectId}_${userId}`; + const hash = simpleHash(combined); + const variation = ((hash % 400) - 200) / 100; // Range: -2 to +2 + return Math.min(100, Math.max(0, score + variation)); } -// Main: Calculate match score for one project and one user +// Main match score calculation function calculateMatchScore(project, user) { - // Weights const weights = { - school: 0.25, - jobTitle: 0.15, - tags: 0.6, + institution: 0.15, + location: 0.05, + interestTags: 0.1, + skillTags: 0.5, + roles: 0.2, }; - const schoolScore = compareValues( - project.institution?.toLowerCase(), - user.college?.toLowerCase() - ); + const institutionScore = compareValues(project.institution, user.college); + const locationScore = compareValues(project.location, user.location); - // Compare against ALL project positions and get best matching role/title - let jobScore = 0; - if (project.positions?.length) { - jobScore = Math.max( + let roleScore = 0; + if (project.positions?.length && Array.isArray(user.roles)) { + roleScore = Math.max( ...project.positions.map((pos) => user.roles.includes(pos.title) ? 1 : 0 ) ); } - // Compare tags: interest_tags and skill_tags const interestScore = compareTags(project.interest_tags, user.interests); const skillScore = compareTags(project.skill_tags, user.skills); - const tagScore = (interestScore + skillScore) / 2; - // Total weighted score const totalScore = - schoolScore * weights.school + - jobScore * weights.jobTitle + - tagScore * weights.tags; + institutionScore * weights.institution + + locationScore * weights.location + + interestScore * weights.interestTags + + skillScore * weights.skillTags + + roleScore * weights.roles; - return Math.round(totalScore * 100); // Normalize to 0–100 + // Apply stable variation + return Math.round(addStableVariation(totalScore * 100, project.id, user.id)); } -// Entry Point: Calculate for all projects +// Entry point export const calculateMatchScores = (userData, projects) => { return projects.map((project) => { const match_percentage = calculateMatchScore(project, userData); From d1d0d9fafc0bee3ddc1a74dd28ab70e03ef8e69a Mon Sep 17 00:00:00 2001 From: Larry La Date: Sat, 19 Apr 2025 22:04:02 -0700 Subject: [PATCH 9/9] fix: page error on empty tags --- .../src/views/IndividualProjectPage.jsx | 58 ++++++++++--------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/frontend/bitmatch/src/views/IndividualProjectPage.jsx b/frontend/bitmatch/src/views/IndividualProjectPage.jsx index 2cd000d..3bdec83 100644 --- a/frontend/bitmatch/src/views/IndividualProjectPage.jsx +++ b/frontend/bitmatch/src/views/IndividualProjectPage.jsx @@ -543,33 +543,39 @@ const ProjectDetailPage = () => {

{project.full_description}

-

Project Categories

- -
- {project.interest_tags.map((interest, index) => ( - - {interest} - - ))} -
- -

Desired Skills

+ {project.interest_tags?.length > 0 && ( +
+

+ Project Categories +

+ {project.interest_tags.map((interest, index) => ( + + {interest} + + ))} +
+ )} -
- {project.skill_tags.map((skill, index) => ( - - {skill} - - ))} -
+ {project.skill_tags?.length > 0 && ( + <> +

Desired Skills

+
+ {project.skill_tags.map((skill, index) => ( + + {skill} + + ))} +
+ + )}