Skip to content

Commit 1c0c5c2

Browse files
committed
Modularisation of Leaderboard subcomponents
1 parent 62ea438 commit 1c0c5c2

File tree

5 files changed

+109
-103
lines changed

5 files changed

+109
-103
lines changed

src/pages/leaderboard/subcomponents/ContestLeaderboard.tsx

Lines changed: 13 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,29 @@ import {
1616

1717
import default_avatar from '../../../assets/default-avatar.jpg';
1818
import leaderboard_background from '../../../assets/leaderboard_background.jpg';
19-
import { Role } from '../../../commons/application/ApplicationTypes';
2019
import LeaderboardDropdown from './LeaderboardDropdown';
20+
import LeaderboardExportButton from './LeaderboardExportButton';
21+
import LeaderboardPodium from './LeaderboardPodium';
2122

2223
type Props = {
2324
type: string;
2425
contestID: number;
2526
};
2627

2728
const ContestLeaderboard: React.FC<Props> = ({ type, contestID }) => {
29+
2830
const courseID = useTypedSelector(store => store.session.courseId);
2931
const dispatch = useDispatch();
32+
33+
// TODO: Only display rows when contest voting counterpart has voting published
34+
3035
// Retrieve Contest Score Data from store
36+
console.log(contestID);
37+
console.log(useTypedSelector(store => store.session.assessmentOverviews));
3138
const rankedLeaderboard: ContestLeaderboardRow[] = useTypedSelector(store =>
3239
type === 'score' ? store.leaderboard.contestScore : store.leaderboard.contestPopularVote
3340
);
41+
3442
useEffect(() => {
3543
if (type === 'score') {
3644
dispatch(LeaderboardActions.getAllContestScores(contestID));
@@ -47,7 +55,8 @@ const ContestLeaderboard: React.FC<Props> = ({ type, contestID }) => {
4755
.map(contest => ({
4856
contest_id: contest.id,
4957
title: contest.title,
50-
published: contest.isPublished
58+
published: contest.isPublished,
59+
voting: contest.hasVotingFeatures
5160
}));
5261

5362
// Temporary loading of leaderboard background
@@ -65,28 +74,6 @@ const ContestLeaderboard: React.FC<Props> = ({ type, contestID }) => {
6574
const top3 = rankedLeaderboard.filter(x => x.rank <= 3);
6675
const rest = rankedLeaderboard.filter(x => x.rank <= Number(visibleEntries) && x.rank > 3);
6776

68-
const role = useTypedSelector(state => state.session.role!);
69-
70-
const exportCSV = () => {
71-
const headers = ['Rank', 'Name', 'Username', 'Score', 'Submission ID'];
72-
const rows = rankedLeaderboard.map(player => [
73-
player.rank,
74-
player.name,
75-
player.username,
76-
player.score,
77-
player.submissionId
78-
]);
79-
80-
// Combine headers and rows
81-
const csvContent = [headers.join(','), ...rows.map(row => row.join(','))].join('\n');
82-
83-
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
84-
const link = document.createElement('a');
85-
link.href = URL.createObjectURL(blob);
86-
link.download = `contest ${type} leaderboard.csv`; // Filename for download
87-
link.click();
88-
};
89-
9077
// const workspaceLocation = 'assessment';
9178
const navigate = useNavigate();
9279
const handleLinkClick = (code: string, votingId: number) => {
@@ -160,33 +147,14 @@ const ContestLeaderboard: React.FC<Props> = ({ type, contestID }) => {
160147
return (
161148
<div className="leaderboard-container">
162149
{/* Top 3 Ranking */}
163-
<div className="top-three">
164-
{top3.slice(0, 3).map((player, index) => (
165-
<div
166-
key={player.username}
167-
className={`top-player ${player.rank === 1 ? 'first' : player.rank === 2 ? 'second' : 'third'}`}
168-
>
169-
<p className="player-name">{player.name}</p>
170-
<div className="player-bar">
171-
<p className="player-rank">{player.rank}</p>
172-
<p className="player-xp">{player.score.toFixed(2)} </p>
173-
</div>
174-
</div>
175-
))}
176-
</div>
150+
<LeaderboardPodium type="contest" data={rankedLeaderboard} outputType="image" />
177151

178152
<div className="buttons-container">
179153
{/* Leaderboard Options Dropdown */}
180154
<LeaderboardDropdown contests={contestDetails} />
181155

182156
{/* Export Button */}
183-
{role === Role.Admin || role === Role.Staff ? (
184-
<button onClick={exportCSV} className="export-button">
185-
Export as .csv
186-
</button>
187-
) : (
188-
''
189-
)}
157+
<LeaderboardExportButton type="contest" data={rankedLeaderboard}/>
190158
</div>
191159

192160
{/* Leaderboard Table (Top 3) */}

src/pages/leaderboard/subcomponents/LeaderboardDropdown.tsx

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,6 @@ import 'src/styles/Leaderboard.scss';
33
import React from 'react';
44
import { useLocation, useNavigate } from 'react-router-dom';
55
import { useTypedSelector } from 'src/commons/utils/Hooks';
6-
/*
7-
import default_avatar from "../../../assets/Sample Profile 1.jpg";
8-
import leaderboard_background from "../../../assets/leaderboard_background.jpg";
9-
10-
import { useTypedSelector } from 'src/commons/utils/Hooks';
11-
import { Role } from '../../../commons/application/ApplicationTypes';
12-
import { useDispatch } from "react-redux";
13-
import LeaderboardActions from "src/features/leaderboard/LeaderboardActions";
14-
*/
156
import { LeaderboardContestDetails } from 'src/features/leaderboard/LeaderboardTypes';
167

178
type Props = {
@@ -38,7 +29,7 @@ const LeaderboardDropdown: React.FC<Props> = ({ contests }) => {
3829

3930
const currentPath = location.pathname;
4031
const publishedContests = enableContestLeaderboard
41-
? contests.filter(contest => contest.published)
32+
? contests.filter(contest => contest.published && !contest.voting)
4233
: [];
4334

4435
return (
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import 'src/styles/Leaderboard.scss';
2+
3+
import React from 'react';
4+
import { ContestLeaderboardRow, LeaderboardRow } from 'src/features/leaderboard/LeaderboardTypes';
5+
import { useTypedSelector } from 'src/commons/utils/Hooks';
6+
import { Role } from '../../../commons/application/ApplicationTypes';
7+
8+
type Props =
9+
| { type: "contest"; data: ContestLeaderboardRow[] }
10+
| { type: "overall"; data: LeaderboardRow[] };
11+
12+
const LeaderboardExportButton: React.FC<Props> = ({ type, data }) => {
13+
const role = useTypedSelector(store => store.session.role);
14+
const exportCSV = () => {
15+
const headers = ['Rank', 'Name', 'Username', 'XP', 'Achievements'];
16+
const rows = data?.map(player => [
17+
player.rank,
18+
player.name,
19+
player.username,
20+
type == "overall" ? (player as LeaderboardRow).xp : (player as ContestLeaderboardRow).score,
21+
type == "overall" ? (player as LeaderboardRow).achievements : (player as ContestLeaderboardRow).submissionId
22+
]);
23+
24+
// Combine headers and rows
25+
const csvContent = [headers.join(','), ...rows.map(row => row.join(','))].join('\n');
26+
27+
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
28+
const link = document.createElement('a');
29+
link.href = URL.createObjectURL(blob);
30+
link.download = 'leaderboard.csv'; // Filename for download
31+
link.click();
32+
};
33+
34+
return role === Role.Admin || role === Role.Staff ? (
35+
<button onClick={exportCSV} className="export-button">
36+
Export as .csv
37+
</button>
38+
) : (
39+
''
40+
);
41+
};
42+
43+
// react-router lazy loading
44+
// https://reactrouter.com/en/main/route/lazy
45+
export const Component = LeaderboardExportButton;
46+
Component.displayName = 'LeaderboardExportButton';
47+
48+
export default LeaderboardExportButton;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import 'src/styles/Leaderboard.scss';
2+
3+
import React from 'react';
4+
import { LeaderboardRow, ContestLeaderboardRow } from 'src/features/leaderboard/LeaderboardTypes';
5+
6+
type Props =
7+
| {type: "overall", data: LeaderboardRow[], outputType: undefined}
8+
| {type: "contest", data: ContestLeaderboardRow[], outputType: "image"}
9+
| {type: "contest", data: ContestLeaderboardRow[], outputType: "audio"};
10+
11+
const LeaderboardPodium: React.FC<Props> = ({ type, data, outputType }) => {
12+
13+
// TODO: Retrieval of rune image/audio files from backend to be displayed on the podium
14+
15+
return (
16+
<div className="top-three-podium">
17+
{data
18+
.filter(x => x.rank <= 3)
19+
.slice(0, 3)
20+
.map((player, index) => (
21+
<div
22+
key={player.username}
23+
className={`top-player ${player.rank === 1 ? 'first' : player.rank === 2 ? 'second' : 'third'}`}
24+
>
25+
<p className="player-name">{player.name}</p>
26+
<div className="player-bar">
27+
<p className="player-rank">{player.rank}</p>
28+
<p className="player-xp">{type == "overall" ? (player as LeaderboardRow).xp : (player as ContestLeaderboardRow).score} XP</p>
29+
</div>
30+
</div>
31+
))}
32+
</div>
33+
);
34+
};
35+
36+
// react-router lazy loading
37+
// https://reactrouter.com/en/main/route/lazy
38+
export const Component = LeaderboardPodium;
39+
Component.displayName = 'LeaderboardPodium';
40+
41+
export default LeaderboardPodium;

src/pages/leaderboard/subcomponents/OverallLeaderboard.tsx

Lines changed: 6 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import {
1515

1616
import default_avatar from '../../../assets/default-avatar.jpg';
1717
import leaderboard_background from '../../../assets/leaderboard_background.jpg';
18-
import { Role } from '../../../commons/application/ApplicationTypes';
1918
import LeaderboardDropdown from './LeaderboardDropdown';
19+
import LeaderboardExportButton from './LeaderboardExportButton';
20+
import LeaderboardPodium from './LeaderboardPodium';
2021

2122
const OverallLeaderboard: React.FC = () => {
2223
// Retrieve XP Data from store
@@ -35,7 +36,8 @@ const OverallLeaderboard: React.FC = () => {
3536
.map(contest => ({
3637
contest_id: contest.id,
3738
title: contest.title,
38-
published: contest.isPublished
39+
published: contest.isPublished,
40+
voting: contest.hasVotingFeatures
3941
}));
4042

4143
// Temporary loading of leaderboard background
@@ -52,28 +54,6 @@ const OverallLeaderboard: React.FC = () => {
5254
const visibleEntries = useTypedSelector(store => store.session.topLeaderboardDisplay);
5355
const topX = rankedLeaderboard.filter(x => x.rank <= Number(visibleEntries));
5456

55-
const role = useTypedSelector(state => state.session.role!);
56-
57-
const exportCSV = () => {
58-
const headers = ['Rank', 'Name', 'Username', 'XP', 'Achievements'];
59-
const rows = rankedLeaderboard.map(player => [
60-
player.rank,
61-
player.name,
62-
player.username,
63-
player.xp,
64-
player.achievements
65-
]);
66-
67-
// Combine headers and rows
68-
const csvContent = [headers.join(','), ...rows.map(row => row.join(','))].join('\n');
69-
70-
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
71-
const link = document.createElement('a');
72-
link.href = URL.createObjectURL(blob);
73-
link.download = 'leaderboard.csv'; // Filename for download
74-
link.click();
75-
};
76-
7757
// Define column definitions for ag-Grid
7858
const columnDefs: ColDef<LeaderboardRow>[] = useMemo(
7959
() => [
@@ -121,36 +101,14 @@ const OverallLeaderboard: React.FC = () => {
121101
return (
122102
<div className="leaderboard-container">
123103
{/* Top 3 Ranking */}
124-
<div className="top-three">
125-
{rankedLeaderboard
126-
.filter(x => x.rank <= 3)
127-
.slice(0, 3)
128-
.map((player, index) => (
129-
<div
130-
key={player.username}
131-
className={`top-player ${player.rank === 1 ? 'first' : player.rank === 2 ? 'second' : 'third'}`}
132-
>
133-
<p className="player-name">{player.name}</p>
134-
<div className="player-bar">
135-
<p className="player-rank">{player.rank}</p>
136-
<p className="player-xp">{player.xp} XP</p>
137-
</div>
138-
</div>
139-
))}
140-
</div>
104+
<LeaderboardPodium type="overall" data={rankedLeaderboard} outputType={undefined}/>
141105

142106
<div className="buttons-container">
143107
{/* Leaderboard Options Dropdown */}
144108
<LeaderboardDropdown contests={contestDetails} />
145109

146110
{/* Export Button */}
147-
{role === Role.Admin || role === Role.Staff ? (
148-
<button onClick={exportCSV} className="export-button">
149-
Export as .csv
150-
</button>
151-
) : (
152-
''
153-
)}
111+
<LeaderboardExportButton type="overall" data={rankedLeaderboard} />
154112
</div>
155113

156114
{/* Leaderboard Table (Replaced with ag-Grid) */}

0 commit comments

Comments
 (0)