Skip to content

Commit 1d9f444

Browse files
authored
Merge pull request #617 from CodebilityDev/fix/dashboard-leaderboard-ranking
fix(leaderboard): rank the dashboard board by points, not by recent activity
2 parents 7e5a6e7 + eaa75a2 commit 1d9f444

1 file changed

Lines changed: 37 additions & 217 deletions

File tree

‎apps/codebility/app/home/(dashboard)/_components/DashboardWeeklyTop.tsx‎

Lines changed: 37 additions & 217 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
TableRow,
1212
} from "@/components/ui/table";
1313
import { createClientClientComponent } from "@/utils/supabase/client";
14-
import { startOfMonth, startOfWeek, subDays, subWeeks, subMonths } from "date-fns";
1514
import { Trophy, Medal, Award, Star, Zap, Heart, Users, Calendar, UserRoundPen } from "lucide-react";
1615

1716
import {
@@ -194,117 +193,23 @@ export default function WeeklyTop() {
194193
if (leaderboardType !== "projects") return;
195194

196195
let isMounted = true;
197-
const supabase = createClientClientComponent();
198-
if (!supabase) return;
199196

200197
const fetchProjectsLeaderboard = async () => {
201198
setIsLoading(true);
202-
203-
try {
204-
let query = supabase
205-
.from("codev_points")
206-
.select(
207-
`
208-
points,
209-
codev_id,
210-
skill_category:skill_category_id!inner(name),
211-
created_at
212-
`,
213-
);
214-
215-
// Removed .gte("created_at") from supabase query level to prevent dropping legacy users.
216-
const { data, error } = await query;
217199

218-
if (!isMounted) return;
200+
try {
201+
const response = await fetch(
202+
`/api/project-leaderboard?timeFilter=${timePeriod}&limit=10`,
203+
);
219204

220-
if (error) {
221-
console.error("Error fetching project points:", error);
222-
return;
205+
if (!response.ok) {
206+
throw new Error("Failed to fetch project leaderboard");
223207
}
224208

225-
if (data) {
226-
227-
// First, get all projects
228-
const { data: projects, error: projectsError } = await supabase
229-
.from("projects")
230-
.select("id, name");
231-
232-
if (!isMounted) return;
233-
234-
if (projectsError) {
235-
console.error("Error fetching projects:", projectsError);
236-
setProjectLeaders([]);
237-
return;
238-
}
239-
240-
// Get project members to map codevs to projects
241-
const { data: projectMembers, error: membersError } = await supabase
242-
.from("project_members")
243-
.select("codev_id, project_id");
244-
245-
if (!isMounted) return;
246-
247-
if (membersError) {
248-
console.error("Error fetching project members:", membersError);
249-
setProjectLeaders([]);
250-
return;
251-
}
252-
253-
// Create a map of codev_id to project_id
254-
const codevToProjectMap = new Map<string, string>();
255-
projectMembers?.forEach((member: any) => {
256-
codevToProjectMap.set(member.codev_id, member.project_id);
257-
});
258-
259-
// Initialize project map with all projects
260-
const projectMap = new Map<string, {
261-
project_id: string;
262-
project_name: string;
263-
total_points: number;
264-
members: Set<string>;
265-
skill_breakdown: Record<string, number>;
266-
}>();
267-
268-
projects?.forEach((project: any) => {
269-
projectMap.set(project.id, {
270-
project_id: project.id,
271-
project_name: project.name,
272-
total_points: 0,
273-
members: new Set(),
274-
skill_breakdown: {}
275-
});
276-
});
277-
278-
// Add points to projects based on member assignments
279-
data.forEach((item: any) => {
280-
const projectId = codevToProjectMap.get(item.codev_id);
281-
if (!projectId || !projectMap.has(projectId)) {
282-
return; // Skip if developer isn't assigned to any project
283-
}
284-
285-
const project = projectMap.get(projectId)!;
286-
const skillCategory = item.skill_category?.name || "Other";
287-
288-
project.total_points += item.points || 0;
289-
project.members.add(item.codev_id);
290-
project.skill_breakdown[skillCategory] = (project.skill_breakdown[skillCategory] || 0) + (item.points || 0);
291-
});
209+
const data = (await response.json()) as { leaders?: ProjectLeader[] };
292210

293-
// Convert to array and sort by total points
294-
const projectsArray: ProjectLeader[] = Array.from(projectMap.values())
295-
.map(project => ({
296-
project_id: project.project_id,
297-
project_name: project.project_name,
298-
total_points: project.total_points,
299-
member_count: project.members.size,
300-
skill_breakdown: project.skill_breakdown
301-
}))
302-
.sort((a, b) => b.total_points - a.total_points)
303-
.slice(0, 10);
304-
305-
if (isMounted) {
306-
setProjectLeaders(projectsArray);
307-
}
211+
if (isMounted) {
212+
setProjectLeaders(data.leaders || []);
308213
}
309214
} catch (error) {
310215
if (isMounted) {
@@ -326,133 +231,42 @@ export default function WeeklyTop() {
326231
}, [leaderboardType, timePeriod]);
327232

328233
useEffect(() => {
329-
if (allCategories.length === 0 || leaderboardType !== "technical") return;
234+
if (leaderboardType !== "technical" || !selectedCategory) return;
330235

331236
let isMounted = true;
332237
const supabase = createClientClientComponent();
333-
if (!supabase) return;
334238

335239
const fetchTopCodevs = async () => {
336240
setIsLoading(true);
337241
try {
338-
let query = supabase
339-
.from("codev_points")
340-
.select(
341-
`
342-
points,
343-
codev_id,
344-
codev:codev_id!inner(first_name, last_name, updated_at),
345-
skill_category:skill_category_id!inner(name),
346-
created_at
347-
`,
348-
)
349-
.order("points", { ascending: false });
350-
351-
// Fetch both codev_points and attendance_points concurrently
352-
// Note: We deliberately query without .gte("created_at") here, shifting boundary checks locally.
353-
const [pointsRes, attendanceRes] = await Promise.all([
354-
query,
355-
supabase.from("attendance_points").select("*")
356-
]);
357-
358-
const { data, error } = pointsRes;
242+
const response = await fetch(
243+
`/api/technical-leaderboard?category=${encodeURIComponent(selectedCategory)}&timeFilter=${timePeriod}&limit=10`,
244+
);
359245

360-
if (error) {
361-
console.error("Error fetching top codevs:", error);
362-
return;
246+
if (!response.ok) {
247+
throw new Error("Failed to fetch technical leaderboard");
363248
}
364249

365-
if (data) {
366-
// Group all records by category first
367-
const groupedData: Record<string, any[]> = {};
368-
369-
allCategories.forEach((category) => {
370-
groupedData[category] = [];
371-
});
250+
const data = (await response.json()) as {
251+
leaders?: { codev_id: string; first_name: string; total_points: number }[];
252+
};
372253

373-
// Build a map of latest attendance activity per user
374-
const latestAttendance = new Map<string, Date>();
375-
if (attendanceRes.data) {
376-
attendanceRes.data.forEach((row) => {
377-
const dateStr = row.last_updated || row.updated_at || row.created_at;
378-
if (dateStr) {
379-
const d = new Date(dateStr);
380-
const current = latestAttendance.get(row.codev_id);
381-
if (!current || d > current) {
382-
latestAttendance.set(row.codev_id, d);
383-
}
384-
}
385-
});
386-
}
387-
388-
// Calculate activity thresholds
389-
const now = new Date();
390-
const fiveMonthsAgo = new Date();
391-
fiveMonthsAgo.setMonth(now.getMonth() - 5);
392-
393-
const weekStart = startOfWeek(subWeeks(new Date(), 1));
394-
const monthStart = startOfMonth(subMonths(new Date(), 1));
395-
396-
const activeThreshold = new Date();
397-
activeThreshold.setDate(now.getDate() - 30); // 30 days of inactivity threshold
398-
399-
data.forEach((item: any) => {
400-
const category = item.skill_category?.name || "Uncategorized";
401-
402-
// Priority Check: Attendance Date > Profile Updated_At
403-
const codevDate = item.codev?.updated_at ? new Date(item.codev.updated_at) : undefined;
404-
const attDate = latestAttendance.get(item.codev_id);
405-
// Defaulting fallback string logic
406-
let userDate = attDate;
407-
if (!userDate || (codevDate && codevDate > userDate)) {
408-
userDate = codevDate;
409-
}
410-
411-
// 1. Core Removal limit
412-
if (!userDate || userDate < fiveMonthsAgo) {
413-
return;
414-
}
415-
416-
// 2. Period Filter limit (Weekly / Monthly boundary)
417-
if (timePeriod === "weekly" && userDate < weekStart) {
418-
return;
419-
} else if (timePeriod === "monthly" && userDate < monthStart) {
420-
return;
421-
}
422-
423-
if (groupedData[category]) {
424-
const isRecentlyActive = userDate >= activeThreshold;
425-
groupedData[category].push({
426-
points: item.points,
427-
codev: item.codev,
428-
skill_category: item.skill_category,
429-
isRecentlyActive,
430-
});
431-
}
432-
});
433-
434-
const finalCategoryData: CategoryData = {};
254+
if (!isMounted) return;
435255

436-
// 2. Sort to prioritize active users, then sort by points, finally slice top 10
437-
Object.keys(groupedData).forEach((category) => {
438-
const categoryArray = groupedData[category];
439-
if (!categoryArray) return;
440-
const sorted = categoryArray.sort((a, b) => {
441-
if (a.isRecentlyActive && !b.isRecentlyActive) return -1;
442-
if (!a.isRecentlyActive && b.isRecentlyActive) return 1;
443-
return b.points - a.points;
444-
});
445-
446-
finalCategoryData[category] = sorted.slice(0, 10);
447-
});
256+
// Ranking is by points alone. All-time totals come from the codev_points ledger;
257+
// weekly/monthly are summed from tasks approved inside the window. Activity is
258+
// deliberately not part of the ordering — a leaderboard ranks scores.
259+
const leaders: TopCodev[] = (data.leaders || []).map((leader) => ({
260+
points: leader.total_points,
261+
codev: { first_name: leader.first_name },
262+
skill_category: { name: selectedCategory },
263+
}));
448264

449-
if (isMounted) {
450-
setCategoryData(finalCategoryData);
451-
}
452-
}
265+
setCategoryData((prev) => ({ ...prev, [selectedCategory]: leaders }));
453266
} catch (error) {
454267
if (isMounted) {
455268
console.error("Error in fetchTopCodevs:", error);
269+
setCategoryData((prev) => ({ ...prev, [selectedCategory]: [] }));
456270
}
457271
} finally {
458272
if (isMounted) {
@@ -463,7 +277,13 @@ export default function WeeklyTop() {
463277

464278
fetchTopCodevs();
465279

466-
// 3. Realtime subscription to points changes
280+
// Realtime subscription so the board reflects newly awarded points.
281+
if (!supabase) {
282+
return () => {
283+
isMounted = false;
284+
};
285+
}
286+
467287
const channel = supabase
468288
.channel("public:codev_points_changes")
469289
.on(
@@ -479,7 +299,7 @@ export default function WeeklyTop() {
479299
isMounted = false;
480300
supabase.removeChannel(channel);
481301
};
482-
}, [timePeriod, allCategories, leaderboardType]);
302+
}, [timePeriod, selectedCategory, leaderboardType]);
483303

484304
const getRankIcon = (rank: number, isTraditional: boolean = true) => {
485305
if (!isTraditional) {

0 commit comments

Comments
 (0)