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/HomePage.jsx b/frontend/bitmatch/src/views/HomePage.jsx index ffc3673..3cd1f4c 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("Something went wrong. Please try again later."); + } 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) { diff --git a/frontend/bitmatch/src/views/IndividualProjectPage.jsx b/frontend/bitmatch/src/views/IndividualProjectPage.jsx index c48a444..3bdec83 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,47 @@ const ProjectDetailPage = () => {

Overview

-

+

Background & More Details About the Project

{project.full_description}

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

+ Project Categories +

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

Desired Skills

+
+ {project.skill_tags.map((skill, index) => ( + + {skill} + + ))} +
+ + )} +
diff --git a/frontend/bitmatch/src/views/MatchScoreUtils.js b/frontend/bitmatch/src/views/MatchScoreUtils.js new file mode 100644 index 0000000..1c66514 --- /dev/null +++ b/frontend/bitmatch/src/views/MatchScoreUtils.js @@ -0,0 +1,91 @@ +// Compare simple string values (e.g., institution, location) +function compareValues(valueA, valueB) { + 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; +} + +// Compare array-type tags (e.g., interests, skills) +function compareTags(tagsA, tagsB) { + 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 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 match score calculation +function calculateMatchScore(project, user) { + const weights = { + institution: 0.15, + location: 0.05, + interestTags: 0.1, + skillTags: 0.5, + roles: 0.2, + }; + + const institutionScore = compareValues(project.institution, user.college); + const locationScore = compareValues(project.location, user.location); + + 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 + ) + ); + } + + const interestScore = compareTags(project.interest_tags, user.interests); + const skillScore = compareTags(project.skill_tags, user.skills); + + const totalScore = + institutionScore * weights.institution + + locationScore * weights.location + + interestScore * weights.interestTags + + skillScore * weights.skillTags + + roleScore * weights.roles; + + // Apply stable variation + return Math.round(addStableVariation(totalScore * 100, project.id, user.id)); +} + +// Entry point +export const calculateMatchScores = (userData, projects) => { + return projects.map((project) => { + const match_percentage = calculateMatchScore(project, userData); + return { + ...project, + match_percentage, + }; + }); +}; diff --git a/frontend/bitmatch/src/views/ProjectListPage.jsx b/frontend/bitmatch/src/views/ProjectListPage.jsx index ff62909..1f320e7 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("Something went wrong. Please try again later."); + } finally { setLoading(false); - }) - .catch((error) => { - setError("Couldn't load projects."); - console.error("Error fetching projects:", error); - setLoading(false); - }); - }, []); + } + }; + + fetchUserAndProjects(); + }, [user.id]); if (loading) { return (