diff --git a/balancer/routes/individualScore.go b/balancer/routes/individualScore.go index 179e65da..ff727125 100644 --- a/balancer/routes/individualScore.go +++ b/balancer/routes/individualScore.go @@ -9,11 +9,11 @@ import ( ) type IndividualScore struct { - Name string `json:"name"` - Score int `json:"score"` - SolvedChallenges int `json:"solvedChallenges"` - Position int `json:"position"` - TotalTeams int `json:"totalTeams"` + Name string `json:"name"` + Score int `json:"score"` + SolvedChallenges []string `json:"solvedChallenges"` + Position int `json:"position"` + TotalTeams int `json:"totalTeams"` } func handleIndividualScore(bundle *b.Bundle) http.Handler { @@ -45,7 +45,7 @@ func handleIndividualScore(bundle *b.Bundle) http.Handler { Score: teamScore.Score, Position: teamScore.Position, TotalTeams: teamCount, - SolvedChallenges: len(teamScore.Challenges), + SolvedChallenges: teamScore.Challenges, } responseBytes, err := json.Marshal(response) diff --git a/balancer/routes/individualScore_test.go b/balancer/routes/individualScore_test.go index a2d3c9ac..2036523f 100644 --- a/balancer/routes/individualScore_test.go +++ b/balancer/routes/individualScore_test.go @@ -59,7 +59,7 @@ func TestIndividualScoreHandler(t *testing.T) { server.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) - assert.JSONEq(t, `{"name":"foobar","score":10,"position":1,"solvedChallenges":1,"totalTeams":1}`, rr.Body.String()) + assert.JSONEq(t, `{"name":"foobar","score":10,"position":1,"solvedChallenges":["scoreBoardChallenge"],"totalTeams":1}`, rr.Body.String()) }) t.Run("returns a 404 if the scores haven't been calculated yet", func(t *testing.T) { diff --git a/balancer/routes/staticFiles.go b/balancer/routes/staticFiles.go index b9c5bd18..60b81bfb 100644 --- a/balancer/routes/staticFiles.go +++ b/balancer/routes/staticFiles.go @@ -14,6 +14,7 @@ func handleStaticFiles(bundle *bundle.Bundle) http.Handler { regexp.MustCompile("/balancer/teams/" + teamNamePatternString + "/status"), regexp.MustCompile("/balancer/teams/" + teamNamePatternString + "/joined"), regexp.MustCompile("/balancer/score-board"), + regexp.MustCompile("/balancer/score-board/teams/" + teamNamePatternString + "/score"), } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/balancer/routes/staticFiles_test.go b/balancer/routes/staticFiles_test.go index 9c038aca..70b70787 100644 --- a/balancer/routes/staticFiles_test.go +++ b/balancer/routes/staticFiles_test.go @@ -34,6 +34,7 @@ func TestStaticFileHandler(t *testing.T) { "/balancer/teams/foo-bar-123/status/", "/balancer/teams/abc/joined/", "/balancer/score-board/", + "/balancer/score-board/teams/abc/score/", } server := http.NewServeMux() diff --git a/balancer/ui/src/App.tsx b/balancer/ui/src/App.tsx index 38abab4b..0032116a 100644 --- a/balancer/ui/src/App.tsx +++ b/balancer/ui/src/App.tsx @@ -4,9 +4,10 @@ import { BrowserRouter, Routes, Route } from "react-router-dom"; import { IntlProvider } from "react-intl"; import { JoinPage } from "./pages/JoinPage"; -import { JoiningPage } from "./pages/JoiningPage"; import { ScoreBoard } from "./pages/ScoreBoard"; +import { JoiningPage } from "./pages/JoiningPage"; import { TeamStatusPage } from "./pages/TeamStatusPage"; +import { IndividualScorePage } from "./pages/IndividualScorePage"; import { Layout } from "./Layout"; import { Spinner } from "./components/Spinner"; @@ -74,6 +75,10 @@ function App() { element={} /> } /> + } + /> diff --git a/balancer/ui/src/pages/IndividualScorePage.tsx b/balancer/ui/src/pages/IndividualScorePage.tsx new file mode 100644 index 00000000..a8a58791 --- /dev/null +++ b/balancer/ui/src/pages/IndividualScorePage.tsx @@ -0,0 +1,77 @@ +import { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; +import { Spinner } from "../components/Spinner"; + +interface IndividualTeamScore { + name: string; + score: string; + position: number; + totalTeams: number; + solvedChallenges: string[]; +} + +async function fetchScore(team: string): Promise { + const response = await fetch(`/balancer/api/score-board/teams/${team}/score`); + return await response.json(); +} + +export function IndividualScorePage() { + const { team } = useParams(); + + if (!team) { + return
Team not found
; + } + + const [score, setScore] = useState(null); + useEffect(() => { + fetchScore(team).then(setScore); + + const timer = setInterval(() => { + fetchScore(team).then(setScore); + }, 5000); + + return () => { + clearInterval(timer); + }; + }, []); + + if (score === null) { + return ; + } + + return ( + <> +
+

+ Solved Challenges for {team} +

+ + + + + + + + {score.solvedChallenges.length === 0 && ( + + + + )} + {score.solvedChallenges.map((challenge) => { + return ( + + + + ); + })} + +
+ Name +
No challenges solved yet
{challenge}
+
+ + ); +} diff --git a/balancer/ui/src/pages/ScoreBoard.tsx b/balancer/ui/src/pages/ScoreBoard.tsx index b20264fc..b78d13b0 100644 --- a/balancer/ui/src/pages/ScoreBoard.tsx +++ b/balancer/ui/src/pages/ScoreBoard.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { injectIntl } from "react-intl"; +import { Link } from "react-router-dom"; function FirstPlace({ ...props }) { return ; @@ -44,7 +44,7 @@ async function fetchTeams(): Promise { return teams; } -export const ScoreBoard = injectIntl(() => { +export function ScoreBoard() { const [teams, setTeams] = useState([]); useEffect(() => { fetchTeams().then(setTeams); @@ -94,7 +94,11 @@ export const ScoreBoard = injectIntl(() => { - {team.name} + + + {team.name} + + {team.score} points

@@ -109,4 +113,4 @@ export const ScoreBoard = injectIntl(() => { ); -}); +}