11import { CLASS_NAME , EXERCISES , STUDENTS } from "@config" ;
2- import { useCallback , useMemo } from "react" ;
2+ import { useCallback , useMemo , useRef } from "react" ;
33import {
44 useGetStudentExercisesQuery ,
55 type StudentExercise ,
@@ -13,10 +13,9 @@ import { useGetStudentsQuery, type Student } from "./api/queries/get_students";
1313function 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