Skip to content

Commit 19673a5

Browse files
authored
Merge pull request #2 from SAN-MUYUN/refactor/progress-dashboard
refactor App.tsx code
2 parents 10b8883 + e3cc528 commit 19673a5

16 files changed

Lines changed: 342 additions & 215 deletions

config/gitmastery.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ export const EXERCISES: string[] = exercisesCsv.map(
99
(row: { exercise_name: string }) => row.exercise_name,
1010
);
1111

12-
export const CLASS_NAME: string | null = null;
12+
export const CLASS_NAME: string | null = "CS2103T";

src/App.tsx

Lines changed: 35 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -1,208 +1,57 @@
1-
import { CLASS_NAME, EXERCISES, STUDENTS } from "@config";
2-
import { useCallback, useMemo, useRef } from "react";
3-
import {
4-
useGetStudentExercisesQuery,
5-
type StudentExercise,
6-
} from "src/api/queries/get_student_exercises";
7-
import {
8-
useGetExercisesQuery,
9-
type Exercise,
10-
} from "./api/queries/get_exercises";
11-
import { useGetStudentsQuery, type Student } from "./api/queries/get_students";
1+
import { useCallback, useRef } from "react";
2+
3+
import { DashboardHeader } from "@/features/student-progress-dashboard/components/DashboardHeader";
4+
import { DownloadCsvButton } from "@/features/student-progress-dashboard/components/DownloadCsvButton";
5+
import { ProgressTable } from "@/features/student-progress-dashboard/components/ProgressTable";
6+
import { useCsvDownload } from "@/features/student-progress-dashboard/hooks/useCsvDownload";
7+
import { useFilteredExercises } from "@/features/student-progress-dashboard/hooks/useFilteredExercises";
8+
import { useFilteredStudents } from "@/features/student-progress-dashboard/hooks/useFilteredStudents";
9+
import { useGetExercisesQuery } from "@/features/student-progress-dashboard/hooks/useGetExercisesQuery";
10+
import { useGetStudentsQuery } from "@/features/student-progress-dashboard/hooks/useGetStudentsQuery";
11+
import type { OnRowComputed, ProgressRow } from "@/features/student-progress-dashboard/types";
1212

1313
function App() {
1414
const { data: allExercises, isLoading: isExercisesLoading } =
1515
useGetExercisesQuery();
1616

17-
const filteredExercises = useMemo(() => {
18-
if (allExercises == null || isExercisesLoading) return [];
19-
const exercisesSet = new Set(EXERCISES);
20-
const exercises =
21-
EXERCISES.length === 0
22-
? allExercises
23-
: allExercises.filter((exercise) =>
24-
exercisesSet.has(exercise.exercise_name),
25-
);
26-
return exercises.sort((a, b) =>
27-
a.exercise_name.localeCompare(b.exercise_name),
28-
);
29-
}, [allExercises, isExercisesLoading]);
30-
17+
const filteredExercises = useFilteredExercises(allExercises, isExercisesLoading);
18+
3119
const { data: allStudents, isLoading: isStudentsLoading } =
3220
useGetStudentsQuery();
3321

34-
const filteredStudents = useMemo(() => {
35-
if (allStudents == null || isStudentsLoading) return [];
36-
const studentsSet = new Set(STUDENTS);
37-
return allStudents.filter((student) => studentsSet.has(student.username));
38-
}, [allStudents, isStudentsLoading]);
39-
40-
const tableDataRef = useRef<
41-
{ username: string; statuses: Record<string, string | undefined> }[]
42-
>([]);
22+
const filteredStudents = useFilteredStudents(allStudents, isStudentsLoading);
4323

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-
);
24+
const tableDataRef = useRef<ProgressRow[]>([]);
5725

58-
const downloadCSV = useCallback(() => {
59-
if (!tableDataRef.current.length) return;
26+
const { downloadCsv } = useCsvDownload({
27+
exercises: filteredExercises,
28+
getRows: () => tableDataRef.current
29+
});
6030

61-
const headers = [
62-
"Github Username",
63-
...filteredExercises.map((e) => e.exercise_name),
31+
const handleRowComputed = useCallback<OnRowComputed>((row) => {
32+
// Replace or add row by username
33+
tableDataRef.current = [
34+
...tableDataRef.current.filter((r) => r.username !== row.username),
35+
row,
6436
];
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]);
37+
}, []);
8338

8439
return (
8540
<div className="w-[80%] mx-auto my-12">
8641
<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>
42+
<DashboardHeader/>
43+
<DownloadCsvButton onClick={downloadCsv} />
9344
</div>
9445

95-
<div className="relative overflow-x-auto">
96-
<table className="w-full text-sm text-left rtl:text-right text-gray-500">
97-
<thead className="text-xs text-gray-700 uppercase bg-gray-50">
98-
<tr>
99-
<th scope="col" className="px-6 py-3">
100-
Github Username
101-
</th>
102-
{filteredExercises.map((exercise) => (
103-
<th key={exercise.exercise_name} className="px-6 py-3">
104-
{exercise.exercise_name}
105-
</th>
106-
))}
107-
</tr>
108-
</thead>
109-
<tbody>
110-
{allStudents != null &&
111-
allExercises != null &&
112-
filteredStudents.map((student) => (
113-
<StudentProgressRow
114-
key={student.id}
115-
student={student}
116-
allStudents={allStudents}
117-
allExercises={allExercises}
118-
filteredExercises={filteredExercises}
119-
onRowComputed={handleRowComputed}
120-
/>
121-
))}
122-
</tbody>
123-
</table>
124-
</div>
125-
</div>
126-
);
127-
}
128-
129-
function StudentProgressRow({
130-
student,
131-
allStudents,
132-
allExercises,
133-
filteredExercises,
134-
onRowComputed,
135-
}: {
136-
student: Student;
137-
allStudents: Student[];
138-
allExercises: Exercise[];
139-
filteredExercises: Exercise[];
140-
onRowComputed: (row: {
141-
username: string;
142-
statuses: Record<string, string | undefined>;
143-
}) => void;
144-
}) {
145-
const { data: studentProgress, isLoading: isStudentProgressLoading } =
146-
useGetStudentExercisesQuery(student.id, allStudents, allExercises);
147-
148-
const latestStatus = useMemo(() => {
149-
if (studentProgress == null || isStudentProgressLoading) {
150-
return new Map<string, StudentExercise>();
151-
}
152-
const result = new Map<string, StudentExercise>();
153-
studentProgress.forEach((exercises, exerciseName: string) => {
154-
result.set(exerciseName, exercises.at(-1)!);
155-
});
156-
return result;
157-
}, [isStudentProgressLoading, studentProgress]);
158-
159-
const getEmoji = useCallback((status?: string | null) => {
160-
switch (status) {
161-
case "SUCCESSFUL":
162-
case "Completed":
163-
return "✅";
164-
case "UNSUCCESSFUL":
165-
case "Incomplete":
166-
return "❌";
167-
case "ERROR":
168-
case "Error":
169-
return "⚠️";
170-
default:
171-
return "";
172-
}
173-
}, []);
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-
]);
46+
<ProgressTable
47+
allStudents={allStudents}
48+
allExercises={allExercises}
49+
filteredStudents={filteredStudents}
50+
filteredExercises={filteredExercises}
51+
onRowComputed={handleRowComputed}
52+
/>
19153

192-
return (
193-
<tr className="bg-white border-b border-gray-200">
194-
<td className="px-6 py-3">{student.username}</td>
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-
})}
205-
</tr>
54+
</div>
20655
);
20756
}
20857

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { CLASS_NAME } from "@config";
2+
3+
export function DashboardHeader() {
4+
const className = CLASS_NAME?.trim();
5+
6+
return (
7+
<h1 className="font-bold text-3xl">
8+
{className ? `${className} ` : ""}Progress Dashboard
9+
</h1>
10+
)
11+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
type DownloadCsvButtonProps = {
2+
onClick: () => void;
3+
disabled?: boolean;
4+
};
5+
6+
export function DownloadCsvButton({ onClick, disabled = false }: DownloadCsvButtonProps) {
7+
return (
8+
<button className="border-2 px-4 py-2 rounded-lg" onClick={onClick} disabled={disabled}>
9+
Download as .csv
10+
</button>
11+
);
12+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { StudentProgressRow } from "@/features/student-progress-dashboard/components/StudentProgressRow";
2+
import { ProgressTableHeader } from "@/features/student-progress-dashboard/components/ProgressTableHeader";
3+
import type {
4+
Exercise,
5+
OnRowComputed,
6+
Student,
7+
} from "@/features/student-progress-dashboard/types";
8+
9+
type ProgressTableProps = {
10+
allStudents: Student[] | undefined;
11+
allExercises: Exercise[] | undefined;
12+
filteredStudents: Student[];
13+
filteredExercises: Exercise[];
14+
onRowComputed: OnRowComputed
15+
};
16+
17+
export function ProgressTable({
18+
allStudents,
19+
allExercises,
20+
filteredStudents,
21+
filteredExercises,
22+
onRowComputed,
23+
}: ProgressTableProps) {
24+
return (
25+
<div className="relative overflow-x-auto">
26+
<table className="w-full text-sm text-left rtl:text-right text-gray-500">
27+
<ProgressTableHeader filteredExercises={filteredExercises} />
28+
<tbody>
29+
{allStudents != null &&
30+
allExercises != null &&
31+
filteredStudents.map((student) => (
32+
<StudentProgressRow
33+
key={student.id}
34+
student={student}
35+
allStudents={allStudents}
36+
allExercises={allExercises}
37+
filteredExercises={filteredExercises}
38+
onRowComputed={onRowComputed}
39+
/>
40+
))}
41+
</tbody>
42+
</table>
43+
</div>
44+
);
45+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { Exercise } from "@/features/student-progress-dashboard/types";
2+
3+
type ProgressTableHeaderProps = {
4+
filteredExercises: Exercise[];
5+
};
6+
7+
export function ProgressTableHeader({ filteredExercises }: ProgressTableHeaderProps) {
8+
return (
9+
<thead className="text-xs text-gray-700 uppercase bg-gray-50">
10+
<tr>
11+
<th scope="col" className="px-6 py-3">
12+
Github Username
13+
</th>
14+
{filteredExercises.map((exercise) => (
15+
<th key={exercise.exercise_name} className="px-6 py-3">
16+
{exercise.exercise_name}
17+
</th>
18+
))}
19+
</tr>
20+
</thead>
21+
);
22+
}

0 commit comments

Comments
 (0)