Skip to content

Commit e94cc50

Browse files
committed
Refactor leaderboard data handling to support pagination and update related actions and types
1 parent 5040583 commit e94cc50

File tree

7 files changed

+85
-57
lines changed

7 files changed

+85
-57
lines changed

src/commons/application/ApplicationTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ export const defaultAchievement: AchievementState = {
351351

352352
export const defaultLeaderboard: LeaderboardState = {
353353
userXp: [],
354-
paginatedUserXp: [],
354+
paginatedUserXp: { rows: [], userCount: 0 },
355355
contestScore: [],
356356
contestPopularVote: [],
357357
code: '',

src/commons/sagas/RequestsSaga.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -489,13 +489,13 @@ export const getAllTotalXp = async (tokens: Tokens): Promise<number | null> => {
489489
};
490490

491491
/**
492-
* GET /courses/{courseId}/all_user_xp
492+
* GET /courses/{courseId}/get_paginated_display/:page/:page_size
493493
*/
494494
export const getPaginatedTotalXp = async (
495495
page: number,
496496
pageSize: number,
497497
tokens: Tokens
498-
): Promise<number | null> => {
498+
): Promise<{ rows: LeaderboardRow[]; userCount: number } | null> => {
499499
const resp = await request(`${courseId()}/get_paginated_display/${page}/${pageSize}`, 'GET', {
500500
...tokens
501501
});
@@ -504,9 +504,9 @@ export const getPaginatedTotalXp = async (
504504
return null; // invalid accessToken _and_ refreshToken
505505
}
506506

507-
const rows = await resp.json();
507+
const data = await resp.json();
508508

509-
return rows.users.map(
509+
const rows = data.users.map(
510510
(row: any): LeaderboardRow => ({
511511
rank: row.rank,
512512
name: row.name,
@@ -516,6 +516,8 @@ export const getPaginatedTotalXp = async (
516516
achievements: ''
517517
})
518518
);
519+
520+
return { rows: rows, userCount: data.total_count };
519521
};
520522

521523
/**

src/features/leaderboard/LeaderboardActions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import {
99
const LeaderboardActions = createActions('leaderboard', {
1010
getAllUsersXp: 0,
1111
saveAllUsersXp: (userXp: LeaderboardRow[]) => userXp,
12-
getPaginatedLeaderboardXp: (page: number, pageSize: number) => ({page, pageSize}),
13-
savePaginatedLeaderboardXp: (paginatedUserXp: LeaderboardRow[]) => paginatedUserXp,
12+
getPaginatedLeaderboardXp: (page: number, pageSize: number) => ({ page, pageSize }),
13+
savePaginatedLeaderboardXp: (payload: { rows: LeaderboardRow[]; userCount: number }) => payload,
1414
getAllContestScores: (assessmentId: number) => assessmentId,
1515
saveAllContestScores: (contestScore: ContestLeaderboardRow[]) => contestScore,
1616
getAllContestPopularVotes: (assessmentId: number) => assessmentId,

src/features/leaderboard/LeaderboardReducer.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ export const LeaderboardReducer: Reducer<LeaderboardState, SourceActionType> = c
1313
state.userXp = action.payload;
1414
})
1515
.addCase(LeaderboardActions.savePaginatedLeaderboardXp, (state, action) => {
16-
state.paginatedUserXp = action.payload;
16+
state.paginatedUserXp = {
17+
rows: action.payload.rows || [],
18+
userCount: action.payload.userCount || 0
19+
};
1720
})
1821
.addCase(LeaderboardActions.saveAllContestScores, (state, action) => {
1922
state.contestScore = action.payload;

src/features/leaderboard/LeaderboardTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export type ContestLeaderboardRow = {
2020

2121
export type LeaderboardState = {
2222
userXp: LeaderboardRow[];
23-
paginatedUserXp: LeaderboardRow[];
23+
paginatedUserXp: { rows: LeaderboardRow[]; userCount: number };
2424
contestScore: ContestLeaderboardRow[];
2525
contestPopularVote: ContestLeaderboardRow[];
2626
code: string;

src/pages/leaderboard/subcomponents/LeaderboardExportButton.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import { ContestLeaderboardRow, LeaderboardRow } from 'src/features/leaderboard/
77
import { Role } from '../../../commons/application/ApplicationTypes';
88

99
type Props =
10-
| { type: string; contest: string | undefined; data: ContestLeaderboardRow[] }
10+
| { type: string; contest: string | undefined; data: ContestLeaderboardRow[] | undefined }
1111
| { type: string; contest: string | undefined; data: LeaderboardRow[] };
1212

1313
const LeaderboardExportButton: React.FC<Props> = ({ type, contest, data }) => {
14+
// pls remove this
15+
if (!data) return;
1416
const role = useTypedSelector(store => store.session.role);
1517
const exportCSV = () => {
1618
const headers = [

src/pages/leaderboard/subcomponents/OverallLeaderboard.tsx

Lines changed: 68 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import 'ag-grid-community/styles/ag-grid.css';
22
import 'ag-grid-community/styles/ag-theme-alpine.css';
33
import 'src/styles/Leaderboard.scss';
44

5-
import { ColDef } from 'ag-grid-community';
5+
import { ColDef, IDatasource } from 'ag-grid-community';
66
import { AgGridReact } from 'ag-grid-react';
7-
import React, { useEffect, useMemo } from 'react';
7+
import React, { useEffect, useMemo, useRef } from 'react';
88
import { useDispatch } from 'react-redux';
99
import default_avatar from 'src/assets/default-avatar.jpg';
1010
import { useTypedSelector } from 'src/commons/utils/Hooks';
@@ -20,28 +20,7 @@ import LeaderboardExportButton from './LeaderboardExportButton';
2020
import LeaderboardPodium from './LeaderboardPodium';
2121

2222
const OverallLeaderboard: React.FC = () => {
23-
// FOR TESTING (To be removed)
2423
const dispatch = useDispatch();
25-
const paginatedLeaderboard: LeaderboardRow[] = useTypedSelector(store => store.leaderboard.paginatedUserXp);
26-
let page = 2;
27-
let pageSize = 25;
28-
useEffect(() => {
29-
dispatch(LeaderboardActions.getPaginatedLeaderboardXp(page, pageSize))
30-
console.log("TEST")
31-
}, [dispatch, page, pageSize]);
32-
33-
useEffect(() => {
34-
console.log(paginatedLeaderboard);
35-
}, [paginatedLeaderboard])
36-
37-
38-
39-
// Retrieve XP Data from store
40-
const rankedLeaderboard: LeaderboardRow[] = useTypedSelector(store => store.leaderboard.userXp);
41-
42-
useEffect(() => {
43-
dispatch(LeaderboardActions.getAllUsersXp());
44-
}, [dispatch]);
4524

4625
// Retrieve contests (For dropdown)
4726
const contestDetails: LeaderboardContestDetails[] = useTypedSelector(
@@ -62,25 +41,6 @@ const OverallLeaderboard: React.FC = () => {
6241
};
6342
}, []);
6443

65-
// Display constants
66-
const visibleEntries = useTypedSelector(store => store.session.topLeaderboardDisplay);
67-
const topX = rankedLeaderboard.slice(0, Number(visibleEntries));
68-
69-
// Set sample profile pictures (Seeded random)
70-
function convertToRandomNumber(id: string): number {
71-
const str = id.slice(1);
72-
let hash = 0;
73-
for (let i = 0; i < str.length; i++) {
74-
const char = str.charCodeAt(i);
75-
hash = (hash << 5) - hash + char;
76-
}
77-
return (Math.abs(hash) % 7) + 1;
78-
}
79-
80-
rankedLeaderboard.map((row: LeaderboardRow) => {
81-
row.avatar = `/assets/Sample_Profile_${convertToRandomNumber(row.username)}.jpg`;
82-
});
83-
8444
// Define column definitions for ag-Grid
8545
const columnDefs: ColDef<LeaderboardRow>[] = useMemo(
8646
() => [
@@ -132,29 +92,90 @@ const OverallLeaderboard: React.FC = () => {
13292
[]
13393
);
13494

95+
const paginatedLeaderboard: { rows: LeaderboardRow[]; userCount: number } = useTypedSelector(store => store.leaderboard.paginatedUserXp);
96+
const pageSize = 25;
97+
const visibleEntries = useTypedSelector(store => store.session.topLeaderboardDisplay) ?? Number.MAX_SAFE_INTEGER;
98+
// const topX = rankedLeaderboard.slice(0, Number(visibleEntries));
99+
100+
useEffect(() => {
101+
dispatch(LeaderboardActions.getPaginatedLeaderboardXp(1, pageSize))
102+
}, [dispatch]);
103+
104+
const latestParamsRef = useRef<any>(null);
105+
const dataSourceRef = useRef<IDatasource>({
106+
getRows: async (params: any) => {
107+
const startRow = params.startRow;
108+
const endRow = params.endRow;
109+
110+
const pageSize = endRow - startRow;
111+
const page = startRow / pageSize + 1
112+
113+
dispatch(LeaderboardActions.getPaginatedLeaderboardXp(page, pageSize));
114+
115+
// Params stored to prevent re-rendering
116+
latestParamsRef.current = params;
117+
},
118+
});
119+
120+
useEffect(() => {
121+
if (
122+
latestParamsRef.current &&
123+
paginatedLeaderboard.rows.length > 0
124+
) {
125+
const { successCallback } = latestParamsRef.current;
126+
successCallback(paginatedLeaderboard.rows, Math.min(paginatedLeaderboard.userCount, visibleEntries));
127+
latestParamsRef.current = null;
128+
}
129+
}, [paginatedLeaderboard]);
130+
131+
// Set sample profile pictures (Seeded random)
132+
function convertToRandomNumber(id: string): number {
133+
const str = id.slice(1);
134+
let hash = 0;
135+
for (let i = 0; i < str.length; i++) {
136+
const char = str.charCodeAt(i);
137+
hash = (hash << 5) - hash + char;
138+
}
139+
return (Math.abs(hash) % 7) + 1;
140+
}
141+
142+
paginatedLeaderboard.rows.map((row: LeaderboardRow) => {
143+
row.avatar = `/assets/Sample_Profile_${convertToRandomNumber(row.username)}.jpg`;
144+
});
145+
146+
const top3Leaderboard = useMemo(() => {
147+
if (paginatedLeaderboard.rows.length > 0) {
148+
return paginatedLeaderboard.rows.slice(0, 3); // Get the top 3 users
149+
}
150+
return []; // Fallback if no data
151+
}, [paginatedLeaderboard.rows]);
152+
135153
return (
136154
<div className="leaderboard-container">
137155
{/* Top 3 Ranking */}
138-
<LeaderboardPodium type="overall" data={rankedLeaderboard} outputType={undefined} />
156+
<LeaderboardPodium type="overall" data={top3Leaderboard} outputType={undefined} />
139157

140158
<div className="buttons-container">
141159
{/* Leaderboard Options Dropdown */}
142160
<LeaderboardDropdown contests={contestDetails} />
143161

144162
{/* Export Button */}
145-
<LeaderboardExportButton type="overall" contest={undefined} data={rankedLeaderboard} />
163+
<LeaderboardExportButton type="overall" contest={undefined} data={undefined} />
146164
</div>
147165

148166
{/* Leaderboard Table (Replaced with ag-Grid) */}
149167
<div className="ag-theme-alpine">
150168
<AgGridReact
151-
rowData={topX}
152-
columnDefs={columnDefs}
153169
pagination={true}
154-
paginationPageSize={25}
155170
paginationPageSizeSelector={[25, 50, 100]}
171+
columnDefs={columnDefs}
172+
rowModelType="infinite"
173+
paginationPageSize={pageSize}
156174
domLayout="autoHeight"
157175
rowHeight={60}
176+
cacheBlockSize={pageSize}
177+
maxBlocksInCache={5}
178+
datasource={dataSourceRef.current}
158179
/>
159180
</div>
160181
</div>

0 commit comments

Comments
 (0)