Skip to content

Commit 5ba4d5a

Browse files
committed
Setup progress dashboard
1 parent 22d941b commit 5ba4d5a

File tree

14 files changed

+819
-100
lines changed

14 files changed

+819
-100
lines changed

index.html

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
<!doctype html>
22
<html lang="en">
3-
<head>
4-
<meta charset="UTF-8" />
5-
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>Vite + React + TS</title>
8-
</head>
9-
<body>
10-
<div id="root"></div>
11-
<script type="module" src="/src/main.tsx"></script>
12-
</body>
3+
4+
<head>
5+
<meta charset="UTF-8" />
6+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8+
<link href="/src/index.css" rel="stylesheet">
9+
<title>Git Mastery Progress Dashboard</title>
10+
</head>
11+
12+
<body>
13+
<div id="root"></div>
14+
<script type="module" src="/src/main.tsx"></script>
15+
</body>
16+
1317
</html>

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@
1010
"preview": "vite preview"
1111
},
1212
"dependencies": {
13+
"@tailwindcss/vite": "^4.0.9",
14+
"axios": "^1.8.1",
1315
"react": "^19.0.0",
14-
"react-dom": "^19.0.0"
16+
"react-dom": "^19.0.0",
17+
"react-query": "^3.39.3",
18+
"react-router": "^7.2.0",
19+
"tailwindcss": "^4.0.9"
1520
},
1621
"devDependencies": {
1722
"@eslint/js": "^9.21.0",

src/App.tsx

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,30 @@
1-
import { useState } from 'react'
2-
import reactLogo from './assets/react.svg'
3-
import viteLogo from '/vite.svg'
4-
import './App.css'
1+
import { useCallback, useRef } from 'react';
2+
import { useNavigate } from 'react-router';
53

64
function App() {
7-
const [count, setCount] = useState(0)
5+
const navigate = useNavigate()
6+
const usernameRef = useRef<HTMLInputElement>(null);
7+
8+
const searchUser = useCallback(() => {
9+
navigate(`/dashboard/${usernameRef?.current?.value}`)
10+
}, [navigate, usernameRef])
811

912
return (
10-
<>
11-
<div>
12-
<a href="https://vite.dev" target="_blank">
13-
<img src={viteLogo} className="logo" alt="Vite logo" />
14-
</a>
15-
<a href="https://react.dev" target="_blank">
16-
<img src={reactLogo} className="logo react" alt="React logo" />
17-
</a>
13+
<div className="w-[40%] my-28 mx-auto">
14+
<div className="text-center mb-6">
15+
<h1 className="text-4xl font-bold mb-4">Git Mastery Progress Dashboard</h1>
16+
<p className="text-gray-700 font-semibold">Enter your Github username to find your progress!</p>
17+
</div>
18+
<div className="flex flex-row border w-full rounded-sm">
19+
<div className="flex items-center justify-center p-4 border-r border-r-gray-700">
20+
<span className="font-bold">@</span>
21+
</div>
22+
<input ref={usernameRef} className="font-semibold w-full px-4 focus:outline-none" placeholder="Your Github username" type="text" />
1823
</div>
19-
<h1>Vite + React</h1>
20-
<div className="card">
21-
<button onClick={() => setCount((count) => count + 1)}>
22-
count is {count}
23-
</button>
24-
<p>
25-
Edit <code>src/App.tsx</code> and save to test HMR
26-
</p>
24+
<div className="text-center mt-4">
25+
<button type="button" onClick={searchUser} className="hover:cursor-pointer hover:bg-gray-600 hover:text-white transition border-1 rounded-sm px-4 py-2 font-semibold">View Progress →</button>
2726
</div>
28-
<p className="read-the-docs">
29-
Click on the Vite and React logos to learn more
30-
</p>
31-
</>
27+
</div>
3228
)
3329
}
3430

src/Dashboard.tsx

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { Link, useParams } from "react-router";
2+
import { useGetUserQuery } from "./api/queries/get_user";
3+
import Spinner from "./components/Spinner";
4+
import { useGetUserProgressQuery } from "./api/queries/get_user_progress";
5+
import { ProblemSet, useGetProblemSetsQuery } from "./api/queries/get_problem_sets";
6+
import { useMemo } from "react";
7+
8+
type UserProblemSetStatus = string;
9+
10+
function Dashboard() {
11+
let { username } = useParams();
12+
13+
const {
14+
data: user,
15+
isLoading: isUserLoading,
16+
} = useGetUserQuery(username);
17+
18+
const {
19+
data: userProgress,
20+
isLoading: isUserProgressLoading,
21+
} = useGetUserProgressQuery(user?.id);
22+
23+
const parsedUserProgress = useMemo(() => {
24+
if (isUserProgressLoading || userProgress == null) {
25+
return new Map<string, UserProblemSetStatus>();
26+
}
27+
28+
const progress = new Map<string, UserProblemSetStatus>();
29+
for (const up of userProgress) {
30+
if (!progress.has(up.exercise_name)) {
31+
progress.set(up.exercise_name, up.status)
32+
} else if (progress.get(up.exercise_name) !== "SUCCESSFUL" && up.status === "SUCCESSFUL") {
33+
// Take any success
34+
progress.set(up.exercise_name, up.status)
35+
}
36+
}
37+
38+
return progress
39+
}, [userProgress, isUserProgressLoading])
40+
41+
const {
42+
data: problemSets,
43+
isLoading: isProblemSetsLoading,
44+
} = useGetProblemSetsQuery();
45+
46+
const problemSetGroups = useMemo(() => {
47+
if (isProblemSetsLoading || problemSets == null) {
48+
return new Map<string, ProblemSet[]>();
49+
}
50+
51+
const repoGroups = new Map<string, ProblemSet[]>();
52+
for (const repo of problemSets) {
53+
for (const topic of repo.topics) {
54+
if (topic !== "problem-set") {
55+
if (parsedUserProgress.has(repo.name)) {
56+
if (!repoGroups.has(topic)) {
57+
repoGroups.set(topic, [])
58+
}
59+
repoGroups.get(topic)!.push(repo)
60+
}
61+
}
62+
}
63+
}
64+
65+
const sortedGroups = new Map([...repoGroups.entries()].sort())
66+
return sortedGroups
67+
}, [problemSets, isProblemSetsLoading, parsedUserProgress])
68+
69+
console.log(problemSetGroups)
70+
71+
return (
72+
<div className="w-[40%] my-28 mx-auto">
73+
<h3 className="text-2xl font-bold mb-4">Git Mastery Progress Dashboard</h3>
74+
<div className="mb-6">
75+
<Link to="/" className="text-gray-500 italic mb-2">← Back to search</Link>
76+
<h1 className="text-4xl font-bold mb-4">@{username}</h1>
77+
<p className="text-gray-700 font-semibold">Find your progress for the various Git Mastery problem sets.</p>
78+
<p className="text-gray-700">To view all problem sets, visit the <a className="text-blue-800 underline" href="https://github.com/git-mastery/problems-directory">problems directory</a>.</p>
79+
</div>
80+
<div>
81+
{(isUserLoading || isUserProgressLoading || isProblemSetsLoading) ? (
82+
<div className="flex justify-center">
83+
<Spinner />
84+
</div>
85+
) : (
86+
user == null ? (
87+
<div className="text-center">
88+
<p className="mb-4 text-red-700">User <strong>{username}</strong> does not exist</p>
89+
<Link to="/" className="hover:cursor-pointer border-1 border-red-700 bg-red-700 text-white rounded-sm px-4 py-2 font-semibold">← Return to search</Link>
90+
</div>
91+
) : (
92+
problemSetGroups.size === 0 ? (
93+
<div className="text-center">
94+
<p className="mb-4">You have not completed any problem sets yet</p>
95+
<a href="https://github.com/git-mastery/problems-directory" target="_blank" className="hover:cursor-pointer border-1 border-blue-800 bg-blue-800 text-white rounded-sm px-4 py-2 font-semibold">Go to problems directory ↗</a>
96+
</div>
97+
) : (
98+
Array.from(problemSetGroups)
99+
.map(([key, value]) => {
100+
return (
101+
<div key={key} className="not-last:mb-4">
102+
<h2 className="text-2xl font-bold mb-2"><code>{key}</code></h2>
103+
<table className="table-fixed w-full bg-white border border-gray-300 rounded-sm">
104+
<thead>
105+
<tr>
106+
<th className="bg-gray-200 border border-gray-300 px-4 py-2 text-left">Exercise name</th>
107+
<th className="bg-gray-200 border border-gray-300 px-4 py-2 text-left">Completion Status</th>
108+
</tr>
109+
</thead>
110+
<tbody>
111+
{value
112+
.filter(problemSet => parsedUserProgress.has(problemSet.name))
113+
.map(problemSet => (
114+
<tr key={problemSet.id}>
115+
<td className="border border-gray-300 px-4 py-2 text-left"><a target="_blank" href={problemSet.html_url}><code className="underline text-blue-800">{problemSet.name}</code></a></td>
116+
<td className="border border-gray-300 px-4 py-2 text-left">{parsedUserProgress.get(problemSet.name)}</td>
117+
</tr>
118+
))}
119+
</tbody>
120+
</table>
121+
</div>
122+
)
123+
})
124+
)
125+
)
126+
)}
127+
</div>
128+
</div >
129+
)
130+
}
131+
132+
export default Dashboard;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import axios from "axios"
2+
import { useQuery } from "react-query"
3+
4+
export interface ProblemSet {
5+
id: number;
6+
name: string;
7+
html_url: string;
8+
forks_count: number;
9+
topics: string[];
10+
}
11+
12+
export const getProblemSets = async () => {
13+
try {
14+
const result = await axios.get<ProblemSet[]>("https://raw.githubusercontent.com/git-mastery/problems-directory/refs/heads/main/problems.json");
15+
return result.data;
16+
} catch {
17+
return [];
18+
}
19+
}
20+
21+
export const useGetProblemSetsQuery = () => {
22+
return useQuery<ProblemSet[]>({
23+
queryKey: ["get-problem-sets"],
24+
queryFn: () => getProblemSets(),
25+
});
26+
}
27+

src/api/queries/get_user.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import axios from "axios"
2+
import { useQuery } from "react-query"
3+
4+
export interface User {
5+
id: number;
6+
}
7+
8+
export const getUser = async (username: string) => {
9+
try {
10+
const user = await axios.get<User>(`https://api.github.com/users/${username}`)
11+
return user.data
12+
} catch {
13+
return null
14+
}
15+
}
16+
17+
export const useGetUserQuery = (username: string | undefined) => {
18+
return useQuery<User | null>({
19+
queryKey: ["get-user", username],
20+
queryFn: () => getUser(username!),
21+
enabled: username != null,
22+
})
23+
}
24+
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import axios from "axios"
2+
import { useQuery } from "react-query"
3+
4+
export interface UserProgress {
5+
exercise_name: string;
6+
status: string;
7+
}
8+
9+
export const getUserProgress = async (userId: number) => {
10+
try {
11+
const result = await axios.get<UserProgress[]>(`https://raw.githubusercontent.com/git-mastery/progress-tracker/refs/heads/main/progress/${userId}.json`);
12+
return result.data;
13+
} catch {
14+
return null;
15+
}
16+
}
17+
18+
export const useGetUserProgressQuery = (userId: number | undefined) => {
19+
return useQuery<UserProgress[] | null>({
20+
queryKey: ["get-user-progress", userId],
21+
queryFn: () => getUserProgress(userId!),
22+
enabled: userId != null,
23+
});
24+
}
25+

src/assets/react.svg

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/components/Spinner.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
function Spinner() {
2+
return (
3+
<div role="status" className="inline-block">
4+
<svg aria-hidden="true" className="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
5+
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor" />
6+
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill" />
7+
</svg>
8+
<span className="sr-only">Loading...</span>
9+
</div>
10+
)
11+
}
12+
13+
export default Spinner;

src/index.css

Lines changed: 2 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,12 @@
1+
@import "tailwindcss";
2+
13
:root {
24
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
35
line-height: 1.5;
46
font-weight: 400;
57

6-
color-scheme: light dark;
7-
color: rgba(255, 255, 255, 0.87);
8-
background-color: #242424;
9-
108
font-synthesis: none;
119
text-rendering: optimizeLegibility;
1210
-webkit-font-smoothing: antialiased;
1311
-moz-osx-font-smoothing: grayscale;
1412
}
15-
16-
a {
17-
font-weight: 500;
18-
color: #646cff;
19-
text-decoration: inherit;
20-
}
21-
a:hover {
22-
color: #535bf2;
23-
}
24-
25-
body {
26-
margin: 0;
27-
display: flex;
28-
place-items: center;
29-
min-width: 320px;
30-
min-height: 100vh;
31-
}
32-
33-
h1 {
34-
font-size: 3.2em;
35-
line-height: 1.1;
36-
}
37-
38-
button {
39-
border-radius: 8px;
40-
border: 1px solid transparent;
41-
padding: 0.6em 1.2em;
42-
font-size: 1em;
43-
font-weight: 500;
44-
font-family: inherit;
45-
background-color: #1a1a1a;
46-
cursor: pointer;
47-
transition: border-color 0.25s;
48-
}
49-
button:hover {
50-
border-color: #646cff;
51-
}
52-
button:focus,
53-
button:focus-visible {
54-
outline: 4px auto -webkit-focus-ring-color;
55-
}
56-
57-
@media (prefers-color-scheme: light) {
58-
:root {
59-
color: #213547;
60-
background-color: #ffffff;
61-
}
62-
a:hover {
63-
color: #747bff;
64-
}
65-
button {
66-
background-color: #f9f9f9;
67-
}
68-
}

0 commit comments

Comments
 (0)