@@ -11,7 +11,6 @@ import {
1111 TableRow ,
1212} from "@/components/ui/table" ;
1313import { createClientClientComponent } from "@/utils/supabase/client" ;
14- import { startOfMonth , startOfWeek , subDays , subWeeks , subMonths } from "date-fns" ;
1514import { Trophy , Medal , Award , Star , Zap , Heart , Users , Calendar , UserRoundPen } from "lucide-react" ;
1615
1716import {
@@ -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