Skip to content

Commit 10b8883

Browse files
committed
Add download button
1 parent 4c8df6f commit 10b8883

File tree

1 file changed

+92
-28
lines changed

1 file changed

+92
-28
lines changed

src/App.tsx

Lines changed: 92 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { CLASS_NAME, EXERCISES, STUDENTS } from "@config";
2-
import { useCallback, useMemo } from "react";
2+
import { useCallback, useMemo, useRef } from "react";
33
import {
44
useGetStudentExercisesQuery,
55
type StudentExercise,
@@ -13,10 +13,9 @@ import { useGetStudentsQuery, type Student } from "./api/queries/get_students";
1313
function App() {
1414
const { data: allExercises, isLoading: isExercisesLoading } =
1515
useGetExercisesQuery();
16+
1617
const filteredExercises = useMemo(() => {
17-
if (allExercises == null || isExercisesLoading) {
18-
return [];
19-
}
18+
if (allExercises == null || isExercisesLoading) return [];
2019
const exercisesSet = new Set(EXERCISES);
2120
const exercises =
2221
EXERCISES.length === 0
@@ -31,20 +30,67 @@ function App() {
3130

3231
const { data: allStudents, isLoading: isStudentsLoading } =
3332
useGetStudentsQuery();
34-
const filteredStudents = useMemo(() => {
35-
if (allStudents == null || isStudentsLoading) {
36-
return [];
37-
}
3833

34+
const filteredStudents = useMemo(() => {
35+
if (allStudents == null || isStudentsLoading) return [];
3936
const studentsSet = new Set(STUDENTS);
4037
return allStudents.filter((student) => studentsSet.has(student.username));
4138
}, [allStudents, isStudentsLoading]);
4239

40+
const tableDataRef = useRef<
41+
{ username: string; statuses: Record<string, string | undefined> }[]
42+
>([]);
43+
44+
const handleRowComputed = useCallback(
45+
(row: {
46+
username: string;
47+
statuses: Record<string, string | undefined>;
48+
}) => {
49+
// Replace or add row by username
50+
tableDataRef.current = [
51+
...tableDataRef.current.filter((r) => r.username !== row.username),
52+
row,
53+
];
54+
},
55+
[],
56+
);
57+
58+
const downloadCSV = useCallback(() => {
59+
if (!tableDataRef.current.length) return;
60+
61+
const headers = [
62+
"Github Username",
63+
...filteredExercises.map((e) => e.exercise_name),
64+
];
65+
const rows = tableDataRef.current.map((row) => [
66+
row.username,
67+
...filteredExercises.map((ex) => row.statuses[ex.exercise_name] ?? ""),
68+
]);
69+
70+
const csvContent =
71+
headers.join(",") + "\n" + rows.map((r) => r.join(",")).join("\n");
72+
73+
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
74+
const url = URL.createObjectURL(blob);
75+
const link = document.createElement("a");
76+
link.href = url;
77+
const timestamp = Math.floor(Date.now() / 1000);
78+
link.setAttribute("download", `progress_${timestamp}.csv`);
79+
document.body.appendChild(link);
80+
link.click();
81+
document.body.removeChild(link);
82+
}, [filteredExercises]);
83+
4384
return (
4485
<div className="w-[80%] mx-auto my-12">
45-
<h1 className="font-bold text-3xl mb-4">
46-
{`${CLASS_NAME != null ? CLASS_NAME + " " : ""}`}Progress Dashboard
47-
</h1>
86+
<div className="flex flex-row justify-between mb-4">
87+
<h1 className="font-bold text-3xl">
88+
{`${CLASS_NAME != null ? CLASS_NAME + " " : ""}`}Progress Dashboard
89+
</h1>
90+
<button className="border-2 px-4 py-2 rounded-lg" onClick={downloadCSV}>
91+
Download as .csv
92+
</button>
93+
</div>
4894

4995
<div className="relative overflow-x-auto">
5096
<table className="w-full text-sm text-left rtl:text-right text-gray-500">
@@ -54,11 +100,7 @@ function App() {
54100
Github Username
55101
</th>
56102
{filteredExercises.map((exercise) => (
57-
<th
58-
key={exercise.exercise_name}
59-
scope="col"
60-
className="px-6 py-3"
61-
>
103+
<th key={exercise.exercise_name} className="px-6 py-3">
62104
{exercise.exercise_name}
63105
</th>
64106
))}
@@ -74,6 +116,7 @@ function App() {
74116
allStudents={allStudents}
75117
allExercises={allExercises}
76118
filteredExercises={filteredExercises}
119+
onRowComputed={handleRowComputed}
77120
/>
78121
))}
79122
</tbody>
@@ -88,11 +131,16 @@ function StudentProgressRow({
88131
allStudents,
89132
allExercises,
90133
filteredExercises,
134+
onRowComputed,
91135
}: {
92136
student: Student;
93137
allStudents: Student[];
94138
allExercises: Exercise[];
95139
filteredExercises: Exercise[];
140+
onRowComputed: (row: {
141+
username: string;
142+
statuses: Record<string, string | undefined>;
143+
}) => void;
96144
}) {
97145
const { data: studentProgress, isLoading: isStudentProgressLoading } =
98146
useGetStudentExercisesQuery(student.id, allStudents, allExercises);
@@ -101,16 +149,14 @@ function StudentProgressRow({
101149
if (studentProgress == null || isStudentProgressLoading) {
102150
return new Map<string, StudentExercise>();
103151
}
104-
105152
const result = new Map<string, StudentExercise>();
106153
studentProgress.forEach((exercises, exerciseName: string) => {
107154
result.set(exerciseName, exercises.at(-1)!);
108155
});
109-
110156
return result;
111157
}, [isStudentProgressLoading, studentProgress]);
112158

113-
const getStatusSymbol = useCallback((status?: string | null) => {
159+
const getEmoji = useCallback((status?: string | null) => {
114160
switch (status) {
115161
case "SUCCESSFUL":
116162
case "Completed":
@@ -125,19 +171,37 @@ function StudentProgressRow({
125171
return "";
126172
}
127173
}, []);
128-
console.log(latestStatus);
129-
console.log(studentProgress);
174+
175+
useMemo(() => {
176+
if (!isStudentProgressLoading) {
177+
const statuses: Record<string, string | undefined> = {};
178+
filteredExercises.forEach((ex) => {
179+
statuses[ex.exercise_name] =
180+
latestStatus.get(ex.exercise_name)?.exerciseProgress?.status ?? "";
181+
});
182+
onRowComputed({ username: student.username, statuses });
183+
}
184+
}, [
185+
student.username,
186+
latestStatus,
187+
filteredExercises,
188+
onRowComputed,
189+
isStudentProgressLoading,
190+
]);
130191

131192
return (
132193
<tr className="bg-white border-b border-gray-200">
133194
<td className="px-6 py-3">{student.username}</td>
134-
{filteredExercises.map((exercise) => (
135-
<td key={exercise.exercise_name} className="px-6 py-3">
136-
{getStatusSymbol(
137-
latestStatus.get(exercise.exercise_name)?.exerciseProgress?.status,
138-
)}
139-
</td>
140-
))}
195+
{filteredExercises.map((exercise) => {
196+
const rawStatus =
197+
latestStatus.get(exercise.exercise_name)?.exerciseProgress?.status ??
198+
"";
199+
return (
200+
<td key={exercise.exercise_name} className="px-6 py-3">
201+
{getEmoji(rawStatus)}
202+
</td>
203+
);
204+
})}
141205
</tr>
142206
);
143207
}

0 commit comments

Comments
 (0)