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 (