|
1 | 1 | "use client"; |
2 | 2 |
|
3 | | -import React, { useState, useEffect } from "react"; |
| 3 | +import React, { useState } from "react"; |
4 | 4 | import { getConfigByRole, endorsementTabs } from "../../config/endorseConfig"; |
5 | | -import SkillsEndorsementTab from "./SkillsEndorsementTab"; |
6 | | -import AchievementsEndorsementTab from "./AchievementsEndorsementTab"; |
7 | 5 | import { AdminContext } from "../../context/AdminContext"; |
8 | | -import api from "../../utils/api"; |
9 | | -const fetchUnendorsedSkills = async (skillType) => { |
10 | | - try { |
11 | | - const res = await api.get( |
12 | | - `/api/skills/user-skills/unendorsed/${skillType}`, |
13 | | - ); |
14 | | - return res.data; |
15 | | - } catch (error) { |
16 | | - const message = |
17 | | - error.response?.data?.message || "Failed to fetch unendorsed skills"; |
18 | | - throw new Error(message); |
19 | | - } |
20 | | -}; |
21 | | - |
22 | | -const endorseSkill = async (skillId) => { |
23 | | - try { |
24 | | - const res = await api.post(`/api/skills/user-skills/endorse/${skillId}`); |
25 | | - return res.data; |
26 | | - } catch (error) { |
27 | | - const message = error.response?.data?.message || "Failed to endorse skill"; |
28 | | - throw new Error(message); |
29 | | - } |
30 | | -}; |
31 | | - |
32 | | -const UserSkillCard = ({ skill, onEndorse, isEndorsing }) => { |
33 | | - const formatDate = (dateString) => { |
34 | | - return new Date(dateString).toLocaleDateString("en-US", { |
35 | | - year: "numeric", |
36 | | - month: "short", |
37 | | - day: "numeric", |
38 | | - }); |
39 | | - }; |
40 | | - |
41 | | - const getProficiencyColor = (level) => { |
42 | | - switch (level.toLowerCase()) { |
43 | | - case "beginner": |
44 | | - return "bg-gray-100 text-gray-800"; |
45 | | - case "intermediate": |
46 | | - return "bg-blue-100 text-blue-800"; |
47 | | - case "advanced": |
48 | | - return "bg-green-100 text-green-800"; |
49 | | - case "expert": |
50 | | - return "bg-purple-100 text-purple-800"; |
51 | | - default: |
52 | | - return "bg-gray-100 text-gray-800"; |
53 | | - } |
54 | | - }; |
55 | | - |
56 | | - return ( |
57 | | - <div className="bg-pink-50 border border-pink-100 rounded-lg p-4 hover:shadow-md transition-shadow duration-200"> |
58 | | - {/* Card Header */} |
59 | | - <div className="mb-4"> |
60 | | - <h3 className="text-lg font-semibold text-gray-900 mb-2"> |
61 | | - {skill.skill_id.name} |
62 | | - </h3> |
63 | | - <p className="text-sm text-gray-600 mb-2">{skill.skill_id.category}</p> |
64 | | - <span |
65 | | - className={`inline-block px-2 py-1 rounded-full text-xs font-medium ${getProficiencyColor(skill.proficiency_level)}`} |
66 | | - > |
67 | | - {skill.proficiency_level.charAt(0).toUpperCase() + |
68 | | - skill.proficiency_level.slice(1)} |
69 | | - </span> |
70 | | - </div> |
71 | | - |
72 | | - {/* User Information */} |
73 | | - <div className="flex items-center gap-2 mb-3 text-sm"> |
74 | | - <div className="w-8 h-8 bg-gray-200 rounded-full flex items-center justify-center"> |
75 | | - <span className="text-xs font-medium text-gray-700"> |
76 | | - {skill.user_id.personal_info.name.charAt(0)} |
77 | | - </span> |
78 | | - </div> |
79 | | - <div> |
80 | | - <p className="font-medium text-gray-900"> |
81 | | - {skill.user_id.personal_info.name} |
82 | | - </p> |
83 | | - <p className="text-sm text-gray-600">{skill.user_id.user_id}</p> |
84 | | - </div> |
85 | | - </div> |
86 | | - |
87 | | - {/* Position and Date Information */} |
88 | | - <div className="space-y-2 mb-2"> |
89 | | - {skill.position_id && ( |
90 | | - <div className="bg-white rounded-md p-2"> |
91 | | - <p className="text-sm font-medium text-gray-900"> |
92 | | - Position: {skill.position_id.title} |
93 | | - </p> |
94 | | - </div> |
95 | | - )} |
96 | | - <div className="flex items-center gap-2 text-sm text-gray-600"> |
97 | | - <svg |
98 | | - className="w-4 h-4" |
99 | | - fill="none" |
100 | | - stroke="currentColor" |
101 | | - viewBox="0 0 24 24" |
102 | | - > |
103 | | - <path |
104 | | - strokeLinecap="round" |
105 | | - strokeLinejoin="round" |
106 | | - strokeWidth={2} |
107 | | - d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" |
108 | | - /> |
109 | | - </svg> |
110 | | - <span>Submitted: {formatDate(skill.created_at)}</span> |
111 | | - </div> |
112 | | - </div> |
113 | | - |
114 | | - {/* Action Button */} |
115 | | - <div className="flex justify-end pt-2 border-t border-pink-200"> |
116 | | - <button |
117 | | - onClick={() => onEndorse(skill._id)} |
118 | | - disabled={isEndorsing} |
119 | | - className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" |
120 | | - > |
121 | | - <span>{isEndorsing ? "Endorsing..." : "Endorse"}</span> |
122 | | - </button> |
123 | | - </div> |
124 | | - </div> |
125 | | - ); |
126 | | -}; |
127 | | - |
128 | | -const UserSkillsTab = ({ skillType }) => { |
129 | | - const [skills, setSkills] = useState([]); |
130 | | - const [loading, setLoading] = useState(true); |
131 | | - const [endorsingSkills, setEndorsingSkills] = useState(new Set()); |
132 | | - const [error, setError] = useState(null); |
133 | 6 |
|
134 | | - useEffect(() => { |
135 | | - loadSkills(); |
136 | | - }, [skillType]); |
137 | | - |
138 | | - const loadSkills = async () => { |
139 | | - try { |
140 | | - setLoading(true); |
141 | | - setError(null); |
142 | | - const data = await fetchUnendorsedSkills(skillType); |
143 | | - setSkills(data); |
144 | | - } catch (err) { |
145 | | - setError("Failed to load user skills. Please try again."); |
146 | | - console.error("Error loading skills:", err); |
147 | | - } finally { |
148 | | - setLoading(false); |
149 | | - } |
150 | | - }; |
151 | | - |
152 | | - const handleEndorse = async (skillId) => { |
153 | | - try { |
154 | | - setEndorsingSkills((prev) => new Set([...prev, skillId])); |
155 | | - |
156 | | - // Make the API call |
157 | | - await endorseSkill(skillId); |
158 | | - |
159 | | - // --- FIX START --- |
160 | | - // Removed the `if (result.success)` condition. |
161 | | - // Now, we update the state to remove the endorsed skill from the list |
162 | | - // as long as the API call above doesn't fail. |
163 | | - setSkills((prev) => prev.filter((skill) => skill._id !== skillId)); |
164 | | - console.log("User skill endorsed successfully"); |
165 | | - // --- FIX END --- |
166 | | - |
167 | | - } catch (err) { |
168 | | - console.error("Error endorsing skill:", err); |
169 | | - setError("Failed to endorse skill. Please try again."); |
170 | | - } finally { |
171 | | - setEndorsingSkills((prev) => { |
172 | | - const newSet = new Set(prev); |
173 | | - newSet.delete(skillId); |
174 | | - return newSet; |
175 | | - }); |
176 | | - } |
177 | | - }; |
178 | | - |
179 | | - if (loading) { |
180 | | - return ( |
181 | | - <div className="flex justify-center items-center py-12"> |
182 | | - <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div> |
183 | | - </div> |
184 | | - ); |
185 | | - } |
186 | | - |
187 | | - if (skills.length === 0) { |
188 | | - return ( |
189 | | - <div className="text-center py-12"> |
190 | | - <div className="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center"> |
191 | | - <svg |
192 | | - className="w-8 h-8 text-gray-400" |
193 | | - fill="none" |
194 | | - stroke="currentColor" |
195 | | - viewBox="0 0 24 24" |
196 | | - > |
197 | | - <path |
198 | | - strokeLinecap="round" |
199 | | - strokeLinejoin="round" |
200 | | - strokeWidth={2} |
201 | | - d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" |
202 | | - /> |
203 | | - </svg> |
204 | | - </div> |
205 | | - <h3 className="text-lg font-medium text-gray-900 mb-2"> |
206 | | - No Pending Endorsements |
207 | | - </h3> |
208 | | - <p className="text-gray-600"> |
209 | | - All {skillType} user skills have been reviewed and endorsed. |
210 | | - </p> |
211 | | - </div> |
212 | | - ); |
213 | | - } |
214 | | - |
215 | | - return ( |
216 | | - <div className="space-y-6"> |
217 | | - {error && ( |
218 | | - <div className="bg-red-50 border border-red-200 rounded-md p-4"> |
219 | | - <div className="flex"> |
220 | | - <svg |
221 | | - className="h-5 w-5 text-red-400" |
222 | | - fill="none" |
223 | | - stroke="currentColor" |
224 | | - viewBox="0 0 24 24" |
225 | | - > |
226 | | - <path |
227 | | - strokeLinecap="round" |
228 | | - strokeLinejoin="round" |
229 | | - strokeWidth={2} |
230 | | - d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" |
231 | | - /> |
232 | | - </svg> |
233 | | - <div className="ml-3"> |
234 | | - <p className="text-sm text-red-800">{error}</p> |
235 | | - </div> |
236 | | - </div> |
237 | | - </div> |
238 | | - )} |
239 | | - |
240 | | - <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> |
241 | | - {skills.map((skill) => ( |
242 | | - <UserSkillCard |
243 | | - key={skill._id} |
244 | | - skill={skill} |
245 | | - onEndorse={handleEndorse} |
246 | | - isEndorsing={endorsingSkills.has(skill._id)} |
247 | | - /> |
248 | | - ))} |
249 | | - </div> |
250 | | - </div> |
251 | | - ); |
252 | | -}; |
| 7 | +// Import the dedicated tab components |
| 8 | +import SkillsEndorsementTab from "./SkillsEndorsementTab"; |
| 9 | +import AchievementsEndorsementTab from "./AchievementsEndorsementTab"; |
| 10 | +import UserSkillsEndorsementTab from "./UserSkillsEndorsementTab"; |
253 | 11 |
|
254 | | -const TabNavigation = ({ activeTab, onTabChange, skillType }) => { |
| 12 | +const TabNavigation = ({ activeTab, onTabChange }) => { |
255 | 13 | const getTabIcon = (iconType) => { |
256 | 14 | switch (iconType) { |
257 | 15 | case "user": |
@@ -334,29 +92,24 @@ const GenSecEndorse = () => { |
334 | 92 | const { userRole } = React.useContext(AdminContext); |
335 | 93 | const adminRole = userRole; |
336 | 94 |
|
337 | | - const [currentPage, setCurrentPage] = useState("endorsement"); // 'endorsement' or 'history' |
338 | 95 | const [activeTab, setActiveTab] = useState("user-skills"); |
339 | 96 |
|
340 | 97 | const config = getConfigByRole(adminRole); |
341 | 98 |
|
342 | | - const handleBackToEndorsement = () => { |
343 | | - setCurrentPage("endorsement"); |
344 | | - }; |
345 | | - |
346 | 99 | const handleTabChange = (tabId) => { |
347 | 100 | setActiveTab(tabId); |
348 | 101 | }; |
349 | 102 |
|
350 | 103 | const renderTabContent = () => { |
351 | 104 | switch (activeTab) { |
352 | 105 | case "user-skills": |
353 | | - return <UserSkillsTab skillType={config.skillType} />; |
| 106 | + return <UserSkillsEndorsementTab skillType={config.skillType} />; |
354 | 107 | case "skills": |
355 | 108 | return <SkillsEndorsementTab skillType={config.skillType} />; |
356 | 109 | case "achievements": |
357 | 110 | return <AchievementsEndorsementTab skillType={config.skillType} />; |
358 | 111 | default: |
359 | | - return <UserSkillsTab skillType={config.skillType} />; |
| 112 | + return <UserSkillsEndorsementTab skillType={config.skillType} />; |
360 | 113 | } |
361 | 114 | }; |
362 | 115 |
|
@@ -403,11 +156,7 @@ const GenSecEndorse = () => { |
403 | 156 | {/* Tab Navigation */} |
404 | 157 | <div className="bg-white border-b border-gray-200"> |
405 | 158 | <div className="max-w-7xl mx-auto"> |
406 | | - <TabNavigation |
407 | | - activeTab={activeTab} |
408 | | - onTabChange={handleTabChange} |
409 | | - skillType={config.skillType} |
410 | | - /> |
| 159 | + <TabNavigation activeTab={activeTab} onTabChange={handleTabChange} /> |
411 | 160 | </div> |
412 | 161 | </div> |
413 | 162 |
|
|
0 commit comments