Skip to content

Commit a315b2e

Browse files
Merge pull request #193 from aviralsaxena16/Added_top_skills_segment
[SPRINT-M25] Implement Top Skills section with API integration and custom hook
2 parents 40e2e00 + 1860488 commit a315b2e

File tree

3 files changed

+177
-64
lines changed

3 files changed

+177
-64
lines changed

backend/routes/skillsRoutes.js

Lines changed: 106 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -6,46 +6,55 @@ const isAuthenticated = require("../middlewares/isAuthenticated");
66
const authorizeRole = require("../middlewares/authorizeRole");
77
const { ROLE_GROUPS } = require("../utils/roles");
88
// GET unendorsed user skills for a particular skill type
9-
router.get("/user-skills/unendorsed/:type",isAuthenticated, async (req, res) => {
10-
const skillType = req.params.type; // e.g. "cultural", "sports"
11-
12-
try {
13-
const skills = await UserSkill.find({ is_endorsed: false })
14-
.populate({
15-
path: "skill_id",
16-
match: { type: skillType },
17-
})
18-
.populate("user_id", "personal_info.name username user_id") // optionally fetch user info
19-
.populate("position_id", "title");
20-
21-
// Filter out null populated skills (i.e., skill type didn't match)
22-
const filtered = skills.filter((us) => us.skill_id !== null);
23-
24-
res.json(filtered);
25-
} catch (err) {
26-
console.error(err);
27-
res.status(500).json({ message: "Error fetching unendorsed skills." });
28-
}
29-
});
30-
31-
router.post("/user-skills/endorse/:id",isAuthenticated, authorizeRole(ROLE_GROUPS.ADMIN), async (req, res) => {
32-
const skillId = req.params.id;
33-
try {
34-
const userSkill = await UserSkill.findById(skillId);
35-
if (!userSkill) {
36-
return res.status(404).json({ message: "User skill not found" });
9+
router.get(
10+
"/user-skills/unendorsed/:type",
11+
isAuthenticated,
12+
async (req, res) => {
13+
const skillType = req.params.type; // e.g. "cultural", "sports"
14+
15+
try {
16+
const skills = await UserSkill.find({ is_endorsed: false })
17+
.populate({
18+
path: "skill_id",
19+
match: { type: skillType },
20+
})
21+
.populate("user_id", "personal_info.name username user_id") // optionally fetch user info
22+
.populate("position_id", "title");
23+
24+
// Filter out null populated skills (i.e., skill type didn't match)
25+
const filtered = skills.filter((us) => us.skill_id !== null);
26+
27+
res.json(filtered);
28+
} catch (err) {
29+
console.error(err);
30+
res.status(500).json({ message: "Error fetching unendorsed skills." });
3731
}
38-
userSkill.is_endorsed = true;
39-
await userSkill.save();
40-
res.json({ message: "User skill endorsed successfully" });
41-
} catch (err) {
42-
console.error(err);
43-
res.status(500).json({ message: "Error endorsing user skill" });
44-
}
45-
});
32+
},
33+
);
34+
35+
router.post(
36+
"/user-skills/endorse/:id",
37+
isAuthenticated,
38+
authorizeRole(ROLE_GROUPS.ADMIN),
39+
async (req, res) => {
40+
const skillId = req.params.id;
41+
try {
42+
const userSkill = await UserSkill.findById(skillId);
43+
if (!userSkill) {
44+
return res.status(404).json({ message: "User skill not found" });
45+
}
46+
userSkill.is_endorsed = true;
47+
await userSkill.save();
48+
res.json({ message: "User skill endorsed successfully" });
49+
} catch (err) {
50+
console.error(err);
51+
res.status(500).json({ message: "Error endorsing user skill" });
52+
}
53+
},
54+
);
4655

4756
// GET all unendorsed skills by type
48-
router.get("/unendorsed/:type",isAuthenticated, async (req, res) => {
57+
router.get("/unendorsed/:type", isAuthenticated, async (req, res) => {
4958
const skillType = req.params.type;
5059

5160
try {
@@ -58,28 +67,33 @@ router.get("/unendorsed/:type",isAuthenticated, async (req, res) => {
5867
});
5968

6069
// POST endorse a skill
61-
router.post("/endorse/:id",isAuthenticated,authorizeRole(ROLE_GROUPS.ADMIN), async (req, res) => {
62-
const skillId = req.params.id;
63-
64-
try {
65-
const skill = await Skill.findById(skillId);
66-
67-
if (!skill) {
68-
return res.status(404).json({ message: "Skill not found" });
70+
router.post(
71+
"/endorse/:id",
72+
isAuthenticated,
73+
authorizeRole(ROLE_GROUPS.ADMIN),
74+
async (req, res) => {
75+
const skillId = req.params.id;
76+
77+
try {
78+
const skill = await Skill.findById(skillId);
79+
80+
if (!skill) {
81+
return res.status(404).json({ message: "Skill not found" });
82+
}
83+
84+
skill.is_endorsed = true;
85+
await skill.save();
86+
87+
res.json({ message: "Skill endorsed successfully", skill });
88+
} catch (err) {
89+
console.error(err);
90+
res.status(500).json({ message: "Failed to endorse skill." });
6991
}
70-
71-
skill.is_endorsed = true;
72-
await skill.save();
73-
74-
res.json({ message: "Skill endorsed successfully", skill });
75-
} catch (err) {
76-
console.error(err);
77-
res.status(500).json({ message: "Failed to endorse skill." });
78-
}
79-
});
92+
},
93+
);
8094

8195
//get all endorsed skills
82-
router.get("/get-skills",isAuthenticated, async (req, res) => {
96+
router.get("/get-skills", isAuthenticated, async (req, res) => {
8397
try {
8498
const skills = await Skill.find({ is_endorsed: true });
8599
res.json(skills);
@@ -90,7 +104,7 @@ router.get("/get-skills",isAuthenticated, async (req, res) => {
90104
});
91105

92106
//get all user skills (endorsed + unendorsed)
93-
router.get("/user-skills/:userId",isAuthenticated, async (req, res) => {
107+
router.get("/user-skills/:userId", isAuthenticated, async (req, res) => {
94108
const userId = req.params.userId;
95109
try {
96110
const userSkills = await UserSkill.find({ user_id: userId })
@@ -110,7 +124,7 @@ router.get("/user-skills/:userId",isAuthenticated, async (req, res) => {
110124
});
111125

112126
//create a new skill
113-
router.post("/create-skill",isAuthenticated, async (req, res) => {
127+
router.post("/create-skill", isAuthenticated, async (req, res) => {
114128
try {
115129
const { name, category, type, description } = req.body;
116130
const skill = new Skill({
@@ -128,7 +142,7 @@ router.post("/create-skill",isAuthenticated, async (req, res) => {
128142
});
129143

130144
//create new user skill
131-
router.post("/create-user-skill",isAuthenticated, async (req, res) => {
145+
router.post("/create-user-skill", isAuthenticated, async (req, res) => {
132146
try {
133147
const { user_id, skill_id, proficiency_level, position_id } = req.body;
134148

@@ -147,4 +161,37 @@ router.post("/create-user-skill",isAuthenticated, async (req, res) => {
147161
}
148162
});
149163

164+
// GET top 5 most popular skills campus-wide
165+
router.get("/top-skills", isAuthenticated, async (req, res) => {
166+
try {
167+
const topSkills = await UserSkill.aggregate([
168+
{ $group: { _id: "$skill_id", totalUsers: { $sum: 1 } } },
169+
{ $sort: { totalUsers: -1 } },
170+
{ $limit: 5 },
171+
{
172+
$lookup: {
173+
from: "skills",
174+
localField: "_id",
175+
foreignField: "_id",
176+
as: "skillDetails",
177+
},
178+
},
179+
{ $unwind: "$skillDetails" },
180+
{
181+
$project: {
182+
_id: 0,
183+
skillName: "$skillDetails.name",
184+
type: "$skillDetails.type",
185+
totalUsers: 1,
186+
},
187+
},
188+
]);
189+
190+
res.json(topSkills);
191+
} catch (err) {
192+
console.error(err);
193+
res.status(500).json({ message: "Error fetching top skills." });
194+
}
195+
});
196+
150197
module.exports = router;
Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,52 @@
1-
import React from 'react'
1+
import React from "react";
2+
import { useTopSkills } from "../../hooks/useTopSkills";
3+
import { Loader2 } from "lucide-react";
24

35
const TopSkills = () => {
6+
const { topSkills, loading } = useTopSkills();
7+
48
return (
5-
<div>TopSkills</div>
6-
)
7-
}
9+
<div className="px-6 pt-6 pb-3 flex flex-col items-start justify-between flex-wrap gap-3 w-full h-full overflow-y-auto">
10+
{/* Header */}
11+
<div className="text-2xl font-bold tracking-tight text-gray-900">
12+
Top 5 Skills
13+
</div>
14+
15+
{/* Loader / Empty / List */}
16+
{loading ? (
17+
<div className="flex justify-center items-center flex-1 w-full">
18+
<Loader2 className="w-5 h-5 animate-spin text-gray-400" />
19+
</div>
20+
) : topSkills.length === 0 ? (
21+
<div className="flex justify-center items-center flex-1 w-full">
22+
<p className="text-gray-500 text-sm text-center">
23+
No skills data available yet.
24+
</p>
25+
</div>
26+
) : (
27+
<ul className="flex flex-col gap-0.5 flex-1 overflow-y-auto w-full -ml-1">
28+
{topSkills.slice(0, 5).map((skill, index) => (
29+
<li
30+
key={index}
31+
className="flex justify-between items-center bg-gray-50 rounded-md px-1 py-0.5 border border-gray-200 hover:bg-gray-100 transition-all"
32+
>
33+
<div className="flex flex-col text-left">
34+
<p className="text-[12.5px] font-medium text-gray-800 truncate leading-tight">
35+
{index + 1}. {skill.skillName}
36+
</p>
37+
<p className="text-[10.5px] text-gray-500 capitalize leading-tight">
38+
{skill.type}
39+
</p>
40+
</div>
41+
<span className="text-[10.5px] font-semibold text-indigo-600 whitespace-nowrap">
42+
{skill.totalUsers} users
43+
</span>
44+
</li>
45+
))}
46+
</ul>
47+
)}
48+
</div>
49+
);
50+
};
851

9-
export default TopSkills
52+
export default TopSkills;

frontend/src/hooks/useTopSkills.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useState, useEffect } from "react";
2+
import api from "../utils/api";
3+
4+
export const useTopSkills = () => {
5+
const [topSkills, setTopSkills] = useState([]);
6+
const [loading, setLoading] = useState(true);
7+
8+
useEffect(() => {
9+
const fetchTopSkills = async () => {
10+
try {
11+
const res = await api.get("/api/skills/top-skills");
12+
setTopSkills(res.data);
13+
} catch (error) {
14+
console.error("Error fetching top skills:", error);
15+
} finally {
16+
setLoading(false);
17+
}
18+
};
19+
fetchTopSkills();
20+
}, []);
21+
22+
return { topSkills, loading };
23+
};

0 commit comments

Comments
 (0)