diff options
Diffstat (limited to 'backend/admin')
| -rw-r--r-- | backend/admin/handler.go | 287 | ||||
| -rw-r--r-- | backend/admin/handler_test.go | 298 | ||||
| -rw-r--r-- | backend/admin/templates/dashboard.html | 3 | ||||
| -rw-r--r-- | backend/admin/templates/tournament_edit.html | 48 | ||||
| -rw-r--r-- | backend/admin/templates/tournament_new.html | 22 | ||||
| -rw-r--r-- | backend/admin/templates/tournaments.html | 20 |
6 files changed, 678 insertions, 0 deletions
diff --git a/backend/admin/handler.go b/backend/admin/handler.go index 6ca0a3f..6e981bc 100644 --- a/backend/admin/handler.go +++ b/backend/admin/handler.go @@ -79,6 +79,12 @@ func (h *Handler) RegisterHandlers(g *echo.Group) { g.GET("/problems/:problemID/testcases/:testcaseID", h.getTestcaseEdit) g.POST("/problems/:problemID/testcases/:testcaseID", h.postTestcaseEdit) g.POST("/problems/:problemID/testcases/:testcaseID/delete", h.postTestcaseDelete) + + g.GET("/tournaments", h.getTournaments) + g.GET("/tournaments/new", h.getTournamentNew) + g.POST("/tournaments/new", h.postTournamentNew) + g.GET("/tournaments/:tournamentID", h.getTournamentEdit) + g.POST("/tournaments/:tournamentID", h.postTournamentEdit) } func (h *Handler) getDashboard(c echo.Context) error { @@ -951,3 +957,284 @@ func (h *Handler) postTestcaseDelete(c echo.Context) error { return c.Redirect(http.StatusSeeOther, h.conf.BasePath+"admin/problems/"+strconv.Itoa(problemID)+"/testcases") } + +func (h *Handler) getTournaments(c echo.Context) error { + rows, err := h.q.ListTournaments(c.Request().Context()) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + tournaments := make([]echo.Map, len(rows)) + for i, t := range rows { + tournaments[i] = echo.Map{ + "TournamentID": t.TournamentID, + "DisplayName": t.DisplayName, + "BracketSize": t.BracketSize, + } + } + + return c.Render(http.StatusOK, "tournaments", echo.Map{ + "BasePath": h.conf.BasePath, + "Title": "Tournaments", + "Tournaments": tournaments, + }) +} + +func (h *Handler) getTournamentNew(c echo.Context) error { + return c.Render(http.StatusOK, "tournament_new", echo.Map{ + "BasePath": h.conf.BasePath, + "Title": "New Tournament", + }) +} + +func nextPowerOf2(n int) int { + p := 1 + for p < n { + p *= 2 + } + return p +} + +func log2Int(n int) int { + r := 0 + for n > 1 { + n /= 2 + r++ + } + return r +} + +func (h *Handler) postTournamentNew(c echo.Context) error { + displayName := c.FormValue("display_name") + numParticipants, err := strconv.Atoi(c.FormValue("num_participants")) + if err != nil || numParticipants < 2 { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid num_participants") + } + + bracketSize := nextPowerOf2(numParticipants) + numRounds := log2Int(bracketSize) + + ctx := c.Request().Context() + err = h.txm.RunInTx(ctx, func(qtx db.Querier) error { + tournamentID, err := qtx.CreateTournament(ctx, db.CreateTournamentParams{ + DisplayName: displayName, + BracketSize: int32(bracketSize), + NumRounds: int32(numRounds), + }) + if err != nil { + return err + } + // Create match slots for all rounds + for round := 0; round < numRounds; round++ { + numPositions := bracketSize / (1 << (round + 1)) + for pos := 0; pos < numPositions; pos++ { + if err := qtx.CreateTournamentMatch(ctx, db.CreateTournamentMatchParams{ + TournamentID: tournamentID, + Round: int32(round), + Position: int32(pos), + }); err != nil { + return err + } + } + } + return nil + }) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + return c.Redirect(http.StatusSeeOther, h.conf.BasePath+"admin/tournaments") +} + +func (h *Handler) getTournamentEdit(c echo.Context) error { + tournamentID, err := strconv.Atoi(c.Param("tournamentID")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid tournament id") + } + ctx := c.Request().Context() + + tournament, err := h.q.GetTournamentByID(ctx, int32(tournamentID)) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return echo.NewHTTPError(http.StatusNotFound) + } + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + entryRows, err := h.q.ListTournamentEntries(ctx, int32(tournamentID)) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + seedUserMap := make(map[int]int) + for _, e := range entryRows { + seedUserMap[int(e.Seed)] = int(e.UserID) + } + + matchRows, err := h.q.ListTournamentMatches(ctx, int32(tournamentID)) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + matchGameMap := make(map[int]int) + var matches []echo.Map + for _, m := range matchRows { + gameID := 0 + if m.GameID != nil { + gameID = int(*m.GameID) + } + matchGameMap[int(m.TournamentMatchID)] = gameID + matches = append(matches, echo.Map{ + "MatchID": int(m.TournamentMatchID), + "Round": int(m.Round), + "Position": int(m.Position), + "Description": "R" + strconv.Itoa(int(m.Round)) + "P" + strconv.Itoa(int(m.Position)), + "IsBye": false, + }) + } + + userRows, err := h.q.ListUsers(ctx) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + var users []echo.Map + for _, r := range userRows { + users = append(users, echo.Map{ + "UserID": int(r.UserID), + "Username": r.Username, + }) + } + + gameRows, err := h.q.ListAllGames(ctx) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + var games []echo.Map + for _, g := range gameRows { + games = append(games, echo.Map{ + "GameID": int(g.GameID), + "DisplayName": g.DisplayName, + }) + } + + seeds := make([]int, tournament.BracketSize) + for i := range seeds { + seeds[i] = i + 1 + } + + return c.Render(http.StatusOK, "tournament_edit", echo.Map{ + "BasePath": h.conf.BasePath, + "Title": "Tournament Edit", + "Tournament": echo.Map{ + "TournamentID": tournament.TournamentID, + "DisplayName": tournament.DisplayName, + "BracketSize": tournament.BracketSize, + "NumRounds": tournament.NumRounds, + }, + "Seeds": seeds, + "SeedUserMap": seedUserMap, + "Matches": matches, + "MatchGameMap": matchGameMap, + "Users": users, + "Games": games, + }) +} + +func (h *Handler) postTournamentEdit(c echo.Context) error { + tournamentID, err := strconv.Atoi(c.Param("tournamentID")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid tournament id") + } + ctx := c.Request().Context() + + tournament, err := h.q.GetTournamentByID(ctx, int32(tournamentID)) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return echo.NewHTTPError(http.StatusNotFound) + } + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + displayName := c.FormValue("display_name") + + // Parse seed → user assignments + type seedEntry struct { + seed int + userID int + } + var seedEntries []seedEntry + for seed := 1; seed <= int(tournament.BracketSize); seed++ { + raw := c.FormValue("seed_" + strconv.Itoa(seed)) + if raw == "" || raw == "0" { + continue + } + userID, err := strconv.Atoi(raw) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid seed_"+strconv.Itoa(seed)) + } + seedEntries = append(seedEntries, seedEntry{seed: seed, userID: userID}) + } + + // Parse match → game assignments + matchRows, err := h.q.ListTournamentMatches(ctx, int32(tournamentID)) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + type matchGame struct { + matchID int + gameID *int32 + } + var matchGames []matchGame + for _, m := range matchRows { + raw := c.FormValue("match_" + strconv.Itoa(int(m.TournamentMatchID))) + var gameID *int32 + if raw != "" && raw != "0" { + gid, err := strconv.Atoi(raw) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid match game") + } + gid32 := int32(gid) + gameID = &gid32 + } + matchGames = append(matchGames, matchGame{matchID: int(m.TournamentMatchID), gameID: gameID}) + } + + err = h.txm.RunInTx(ctx, func(qtx db.Querier) error { + if err := qtx.UpdateTournament(ctx, db.UpdateTournamentParams{ + TournamentID: int32(tournamentID), + DisplayName: displayName, + BracketSize: tournament.BracketSize, + NumRounds: tournament.NumRounds, + }); err != nil { + return err + } + + // Replace entries + if err := qtx.DeleteTournamentEntries(ctx, int32(tournamentID)); err != nil { + return err + } + for _, se := range seedEntries { + if err := qtx.CreateTournamentEntry(ctx, db.CreateTournamentEntryParams{ + TournamentID: int32(tournamentID), + UserID: int32(se.userID), + Seed: int32(se.seed), + }); err != nil { + return err + } + } + + // Update match game assignments + for _, mg := range matchGames { + if err := qtx.UpdateTournamentMatchGame(ctx, db.UpdateTournamentMatchGameParams{ + TournamentMatchID: int32(mg.matchID), + GameID: mg.gameID, + }); err != nil { + return err + } + } + + return nil + }) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + return c.Redirect(http.StatusSeeOther, h.conf.BasePath+"admin/tournaments") +} diff --git a/backend/admin/handler_test.go b/backend/admin/handler_test.go index 20c45ef..0e13c60 100644 --- a/backend/admin/handler_test.go +++ b/backend/admin/handler_test.go @@ -44,6 +44,16 @@ type mockQuerier struct { getTestcaseResultsBySubmIDFunc func(ctx context.Context, submissionID int32) ([]db.TestcaseResult, error) listTestcasesByGameIDFunc func(ctx context.Context, gameID int32) ([]db.Testcase, error) updateGameStartedAtFunc func(ctx context.Context, arg db.UpdateGameStartedAtParams) error + listTournamentsFunc func(ctx context.Context) ([]db.Tournament, error) + getTournamentByIDFunc func(ctx context.Context, tournamentID int32) (db.Tournament, error) + createTournamentFunc func(ctx context.Context, arg db.CreateTournamentParams) (int32, error) + updateTournamentFunc func(ctx context.Context, arg db.UpdateTournamentParams) error + listTournamentEntriesFunc func(ctx context.Context, tournamentID int32) ([]db.ListTournamentEntriesRow, error) + deleteTournamentEntriesFunc func(ctx context.Context, tournamentID int32) error + createTournamentEntryFunc func(ctx context.Context, arg db.CreateTournamentEntryParams) error + listTournamentMatchesFunc func(ctx context.Context, tournamentID int32) ([]db.TournamentMatch, error) + createTournamentMatchFunc func(ctx context.Context, arg db.CreateTournamentMatchParams) error + updateTournamentMatchGameFunc func(ctx context.Context, arg db.UpdateTournamentMatchGameParams) error } func (m *mockQuerier) GetUserByID(ctx context.Context, userID int32) (db.User, error) { @@ -204,6 +214,76 @@ func (m *mockQuerier) ListTestcasesByProblemIDForUpdate(ctx context.Context, pro return m.ListTestcasesByProblemID(ctx, problemID) } +func (m *mockQuerier) ListTournaments(ctx context.Context) ([]db.Tournament, error) { + if m.listTournamentsFunc != nil { + return m.listTournamentsFunc(ctx) + } + return nil, nil +} + +func (m *mockQuerier) GetTournamentByID(ctx context.Context, tournamentID int32) (db.Tournament, error) { + if m.getTournamentByIDFunc != nil { + return m.getTournamentByIDFunc(ctx, tournamentID) + } + return db.Tournament{}, pgx.ErrNoRows +} + +func (m *mockQuerier) CreateTournament(ctx context.Context, arg db.CreateTournamentParams) (int32, error) { + if m.createTournamentFunc != nil { + return m.createTournamentFunc(ctx, arg) + } + return 1, nil +} + +func (m *mockQuerier) UpdateTournament(ctx context.Context, arg db.UpdateTournamentParams) error { + if m.updateTournamentFunc != nil { + return m.updateTournamentFunc(ctx, arg) + } + return nil +} + +func (m *mockQuerier) ListTournamentEntries(ctx context.Context, tournamentID int32) ([]db.ListTournamentEntriesRow, error) { + if m.listTournamentEntriesFunc != nil { + return m.listTournamentEntriesFunc(ctx, tournamentID) + } + return nil, nil +} + +func (m *mockQuerier) DeleteTournamentEntries(ctx context.Context, tournamentID int32) error { + if m.deleteTournamentEntriesFunc != nil { + return m.deleteTournamentEntriesFunc(ctx, tournamentID) + } + return nil +} + +func (m *mockQuerier) CreateTournamentEntry(ctx context.Context, arg db.CreateTournamentEntryParams) error { + if m.createTournamentEntryFunc != nil { + return m.createTournamentEntryFunc(ctx, arg) + } + return nil +} + +func (m *mockQuerier) ListTournamentMatches(ctx context.Context, tournamentID int32) ([]db.TournamentMatch, error) { + if m.listTournamentMatchesFunc != nil { + return m.listTournamentMatchesFunc(ctx, tournamentID) + } + return nil, nil +} + +func (m *mockQuerier) CreateTournamentMatch(ctx context.Context, arg db.CreateTournamentMatchParams) error { + if m.createTournamentMatchFunc != nil { + return m.createTournamentMatchFunc(ctx, arg) + } + return nil +} + +func (m *mockQuerier) UpdateTournamentMatchGame(ctx context.Context, arg db.UpdateTournamentMatchGameParams) error { + if m.updateTournamentMatchGameFunc != nil { + return m.updateTournamentMatchGameFunc(ctx, arg) + } + return nil +} + // mockTxManager implements db.TxManager for testing. type mockTxManager struct { runInTxFunc func(ctx context.Context, fn func(q db.Querier) error) error @@ -1062,3 +1142,221 @@ func TestGetSubmissionDetail_NotFound(t *testing.T) { t.Errorf("status = %d, want %d", httpErr.Code, http.StatusNotFound) } } + +// --- Tournament admin tests --- + +func TestNextPowerOf2(t *testing.T) { + tests := []struct { + input int + expected int + }{ + {2, 2}, + {3, 4}, + {4, 4}, + {5, 8}, + {6, 8}, + {7, 8}, + {8, 8}, + {9, 16}, + } + for _, tt := range tests { + got := nextPowerOf2(tt.input) + if got != tt.expected { + t.Errorf("nextPowerOf2(%d) = %d, want %d", tt.input, got, tt.expected) + } + } +} + +func TestLog2Int(t *testing.T) { + tests := []struct { + input int + expected int + }{ + {1, 0}, + {2, 1}, + {4, 2}, + {8, 3}, + {16, 4}, + } + for _, tt := range tests { + got := log2Int(tt.input) + if got != tt.expected { + t.Errorf("log2Int(%d) = %d, want %d", tt.input, got, tt.expected) + } + } +} + +func TestGetTournaments_Empty(t *testing.T) { + h := newTestHandler(&mockQuerier{}) + c, rec := newEchoContext(http.MethodGet, "/admin/tournaments", nil) + err := h.getTournaments(c) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if rec.Code != http.StatusOK { + t.Errorf("status = %d, want %d", rec.Code, http.StatusOK) + } +} + +func TestGetTournaments_WithData(t *testing.T) { + h := newTestHandler(&mockQuerier{ + listTournamentsFunc: func(_ context.Context) ([]db.Tournament, error) { + return []db.Tournament{ + {TournamentID: 1, DisplayName: "Tournament A", BracketSize: 4}, + {TournamentID: 2, DisplayName: "Tournament B", BracketSize: 8}, + }, nil + }, + }) + c, _ := newEchoContext(http.MethodGet, "/admin/tournaments", nil) + err := h.getTournaments(c) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestGetTournamentNew(t *testing.T) { + h := newTestHandler(&mockQuerier{}) + c, rec := newEchoContext(http.MethodGet, "/admin/tournaments/new", nil) + err := h.getTournamentNew(c) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if rec.Code != http.StatusOK { + t.Errorf("status = %d, want %d", rec.Code, http.StatusOK) + } +} + +func TestPostTournamentNew_Success(t *testing.T) { + var createdParams db.CreateTournamentParams + var matchCount int + h := &Handler{ + q: &mockQuerier{}, + txm: &mockTxManager{ + runInTxFunc: func(_ context.Context, fn func(q db.Querier) error) error { + return fn(&mockQuerier{ + createTournamentFunc: func(_ context.Context, arg db.CreateTournamentParams) (int32, error) { + createdParams = arg + return 1, nil + }, + createTournamentMatchFunc: func(_ context.Context, _ db.CreateTournamentMatchParams) error { + matchCount++ + return nil + }, + }) + }, + }, + conf: &config.Config{BasePath: "/test/"}, + } + form := url.Values{ + "display_name": {"Test Tournament"}, + "num_participants": {"3"}, + } + c, rec := newEchoContextWithForm("/admin/tournaments/new", nil, form) + err := h.postTournamentNew(c) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if rec.Code != http.StatusSeeOther { + t.Errorf("status = %d, want %d", rec.Code, http.StatusSeeOther) + } + if createdParams.DisplayName != "Test Tournament" { + t.Errorf("display_name = %q, want %q", createdParams.DisplayName, "Test Tournament") + } + if createdParams.BracketSize != 4 { + t.Errorf("bracket_size = %d, want 4", createdParams.BracketSize) + } + if createdParams.NumRounds != 2 { + t.Errorf("num_rounds = %d, want 2", createdParams.NumRounds) + } + // bracket_size=4, num_rounds=2 → round 0: 2 matches + round 1: 1 match = 3 matches + if matchCount != 3 { + t.Errorf("match count = %d, want 3", matchCount) + } +} + +func TestPostTournamentNew_InvalidParticipants(t *testing.T) { + h := newTestHandler(&mockQuerier{}) + form := url.Values{ + "display_name": {"Test"}, + "num_participants": {"abc"}, + } + c, _ := newEchoContextWithForm("/admin/tournaments/new", nil, form) + err := h.postTournamentNew(c) + if err == nil { + t.Fatal("expected error for invalid num_participants") + } + httpErr, ok := err.(*echo.HTTPError) + if !ok { + t.Fatalf("expected echo.HTTPError, got %T", err) + } + if httpErr.Code != http.StatusBadRequest { + t.Errorf("status = %d, want %d", httpErr.Code, http.StatusBadRequest) + } +} + +func TestGetTournamentEdit_NotFound(t *testing.T) { + h := newTestHandler(&mockQuerier{}) + c, _ := newEchoContext(http.MethodGet, "/admin/tournaments/999", map[string]string{ + "tournamentID": "999", + }) + err := h.getTournamentEdit(c) + if err == nil { + t.Fatal("expected error for non-existent tournament") + } + httpErr, ok := err.(*echo.HTTPError) + if !ok { + t.Fatalf("expected echo.HTTPError, got %T", err) + } + if httpErr.Code != http.StatusNotFound { + t.Errorf("status = %d, want %d", httpErr.Code, http.StatusNotFound) + } +} + +func TestGetTournamentEdit_Success(t *testing.T) { + h := newTestHandler(&mockQuerier{ + getTournamentByIDFunc: func(_ context.Context, _ int32) (db.Tournament, error) { + return db.Tournament{ + TournamentID: 1, + DisplayName: "Test", + BracketSize: 4, + NumRounds: 2, + }, nil + }, + listTournamentMatchesFunc: func(_ context.Context, _ int32) ([]db.TournamentMatch, error) { + return []db.TournamentMatch{ + {TournamentMatchID: 1, Round: 0, Position: 0}, + }, nil + }, + }) + c, rec := newEchoContext(http.MethodGet, "/admin/tournaments/1", map[string]string{ + "tournamentID": "1", + }) + err := h.getTournamentEdit(c) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if rec.Code != http.StatusOK { + t.Errorf("status = %d, want %d", rec.Code, http.StatusOK) + } +} + +func TestPostTournamentEdit_NotFound(t *testing.T) { + h := newTestHandler(&mockQuerier{}) + form := url.Values{ + "display_name": {"Updated"}, + } + c, _ := newEchoContextWithForm("/admin/tournaments/999", map[string]string{ + "tournamentID": "999", + }, form) + err := h.postTournamentEdit(c) + if err == nil { + t.Fatal("expected error for non-existent tournament") + } + httpErr, ok := err.(*echo.HTTPError) + if !ok { + t.Fatalf("expected echo.HTTPError, got %T", err) + } + if httpErr.Code != http.StatusNotFound { + t.Errorf("status = %d, want %d", httpErr.Code, http.StatusNotFound) + } +} diff --git a/backend/admin/templates/dashboard.html b/backend/admin/templates/dashboard.html index 5a7b484..2e9336a 100644 --- a/backend/admin/templates/dashboard.html +++ b/backend/admin/templates/dashboard.html @@ -11,6 +11,9 @@ <a href="{{ .BasePath }}admin/problems">Problems</a> </p> <p> + <a href="{{ .BasePath }}admin/tournaments">Tournaments</a> +</p> +<p> <a href="{{ .BasePath }}admin/online-qualifying-ranking">Online Qualifying Ranking</a> </p> <form method="post" action="{{ .BasePath }}admin/fix"> diff --git a/backend/admin/templates/tournament_edit.html b/backend/admin/templates/tournament_edit.html new file mode 100644 index 0000000..6c1ad6c --- /dev/null +++ b/backend/admin/templates/tournament_edit.html @@ -0,0 +1,48 @@ +{{ template "base.html" . }} + +{{ define "breadcrumb" }} +<a href="{{ .BasePath }}admin/dashboard">Dashboard</a> | <a href="{{ .BasePath }}admin/tournaments">Tournaments</a> +{{ end }} + +{{ define "content" }} +<form method="post"> + <div> + <label>Display Name</label> + <input type="text" name="display_name" value="{{ .Tournament.DisplayName }}" required> + </div> + + <h3>Bracket (size={{ .Tournament.BracketSize }}, rounds={{ .Tournament.NumRounds }})</h3> + + <h3>Entries (seed → user)</h3> + {{ range .Seeds }} + {{ $seed := . }} + <div> + <label>Seed {{ $seed }}</label> + <select name="seed_{{ $seed }}"> + <option value="0">-- none (bye) --</option> + {{ range $.Users }} + <option value="{{ .UserID }}" {{ if eq .UserID (index $.SeedUserMap $seed) }}selected{{ end }}>{{ .Username }} (id={{ .UserID }})</option> + {{ end }} + </select> + </div> + {{ end }} + + <h3>Matches (game assignment)</h3> + {{ range .Matches }} + <div> + <label>Round {{ .Round }} Pos {{ .Position }} ({{ .Description }})</label> + {{ $matchID := .MatchID }} + <select name="match_{{ $matchID }}"> + <option value="0">-- no game --</option> + {{ range $.Games }} + <option value="{{ .GameID }}" {{ if eq .GameID (index $.MatchGameMap $matchID) }}selected{{ end }}>{{ .DisplayName }} (id={{ .GameID }})</option> + {{ end }} + </select> + </div> + {{ end }} + + <div> + <button type="submit">Save</button> + </div> +</form> +{{ end }} diff --git a/backend/admin/templates/tournament_new.html b/backend/admin/templates/tournament_new.html new file mode 100644 index 0000000..14b0c4c --- /dev/null +++ b/backend/admin/templates/tournament_new.html @@ -0,0 +1,22 @@ +{{ template "base.html" . }} + +{{ define "breadcrumb" }} +<a href="{{ .BasePath }}admin/dashboard">Dashboard</a> | <a href="{{ .BasePath }}admin/tournaments">Tournaments</a> +{{ end }} + +{{ define "content" }} +<form method="post"> + <div> + <label>Display Name</label> + <input type="text" name="display_name" required> + </div> + <div> + <label>Number of Participants</label> + <input type="number" name="num_participants" min="2" max="64" value="4" required> + <small>Bracket size will be rounded up to next power of 2.</small> + </div> + <div> + <button type="submit">Create</button> + </div> +</form> +{{ end }} diff --git a/backend/admin/templates/tournaments.html b/backend/admin/templates/tournaments.html new file mode 100644 index 0000000..09989a2 --- /dev/null +++ b/backend/admin/templates/tournaments.html @@ -0,0 +1,20 @@ +{{ template "base.html" . }} + +{{ define "breadcrumb" }} +<a href="{{ .BasePath }}admin/dashboard">Dashboard</a> +{{ end }} + +{{ define "content" }} +<div> + <a href="{{ .BasePath }}admin/tournaments/new">Create New Tournament</a> +</div> +<ul> + {{ range .Tournaments }} + <li> + <a href="{{ $.BasePath }}admin/tournaments/{{ .TournamentID }}"> + {{ .DisplayName }} (id={{ .TournamentID }} bracket_size={{ .BracketSize }}) + </a> + </li> + {{ end }} +</ul> +{{ end }} |
