diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-09-06 01:09:20 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-09-06 01:09:20 +0900 |
| commit | e7b35cd81f2e515371a30a59ea4173a31dbeefc5 (patch) | |
| tree | 783ef649110730a8946b0021c50688d29b6dab9b | |
| parent | e33bfff4db95586a3140b5e71a7d3dba2c72f694 (diff) | |
| download | iosdc-japan-2025-albatross-e7b35cd81f2e515371a30a59ea4173a31dbeefc5.tar.gz iosdc-japan-2025-albatross-e7b35cd81f2e515371a30a59ea4173a31dbeefc5.tar.zst iosdc-japan-2025-albatross-e7b35cd81f2e515371a30a59ea4173a31dbeefc5.zip | |
feat(backend): add admin page for game creation
| -rw-r--r-- | backend/admin/handler.go | 149 | ||||
| -rw-r--r-- | backend/admin/templates/game_edit.html | 8 | ||||
| -rw-r--r-- | backend/admin/templates/game_new.html | 40 | ||||
| -rw-r--r-- | backend/admin/templates/games.html | 3 | ||||
| -rw-r--r-- | backend/db/query.sql.go | 27 | ||||
| -rw-r--r-- | backend/query.sql | 5 |
6 files changed, 191 insertions, 41 deletions
diff --git a/backend/admin/handler.go b/backend/admin/handler.go index 5a0f0a1..19e44e6 100644 --- a/backend/admin/handler.go +++ b/backend/admin/handler.go @@ -62,6 +62,8 @@ func (h *Handler) RegisterHandlers(g *echo.Group) { g.POST("/users/:userID/fetch-icon", h.postUserFetchIcon) g.GET("/games", h.getGames) + g.GET("/games/new", h.getGameNew) + g.POST("/games/new", h.postGameNew) g.GET("/games/:gameID", h.getGameEdit) g.POST("/games/:gameID", h.postGameEdit) g.POST("/games/:gameID/start", h.postGameStart) @@ -80,6 +82,44 @@ func (h *Handler) getDashboard(c echo.Context) error { }) } +func (h *Handler) getOnlineQualifyingRanking(c echo.Context) error { + game1, err := strconv.Atoi(c.QueryParam("game_1")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid game_1") + } + game2, err := strconv.Atoi(c.QueryParam("game_2")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid game_2") + } + + rows, err := h.q.GetQualifyingRanking(c.Request().Context(), db.GetQualifyingRankingParams{ + GameID: int32(game1), + GameID_2: int32(game2), + }) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + entries := make([]echo.Map, len(rows)) + for i, r := range rows { + entries[i] = echo.Map{ + "Rank": i + 1, + "Username": r.Username, + "UserLabel": r.UserLabel, + "Score1": r.CodeSize1, + "Score2": r.CodeSize2, + "TotalScore": r.TotalCodeSize, + "SubmittedAt1": r.SubmittedAt1.Time.In(jst).Format("2006-01-02T15:04"), + "SubmittedAt2": r.SubmittedAt2.Time.In(jst).Format("2006-01-02T15:04"), + } + } + return c.Render(http.StatusOK, "online_qualifying_ranking", echo.Map{ + "BasePath": h.conf.BasePath, + "Title": "Online Qualifying Ranking", + "Entries": entries, + }) +} + func (h *Handler) getUsers(c echo.Context) error { rows, err := h.q.ListUsers(c.Request().Context()) if err != nil { @@ -215,6 +255,60 @@ func (h *Handler) getGames(c echo.Context) error { }) } +func (h *Handler) getGameNew(c echo.Context) error { + problemRows, err := h.q.ListProblems(c.Request().Context()) + if err != nil { + if !errors.Is(err, pgx.ErrNoRows) { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + } + var problems []echo.Map + for _, p := range problemRows { + problems = append(problems, echo.Map{ + "ProblemID": int(p.ProblemID), + "Title": p.Title, + }) + } + + return c.Render(http.StatusOK, "game_new", echo.Map{ + "BasePath": h.conf.BasePath, + "Title": "New Game", + "Problems": problems, + }) +} + +func (h *Handler) postGameNew(c echo.Context) error { + gameType := c.FormValue("game_type") + isPublic := (c.FormValue("is_public") != "") + displayName := c.FormValue("display_name") + durationSeconds, err := strconv.Atoi(c.FormValue("duration_seconds")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid duration_seconds") + } + var problemID int + { + problemIDRaw := c.FormValue("problem_id") + problemIDInt, err := strconv.Atoi(problemIDRaw) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid problem_id") + } + problemID = problemIDInt + } + + _, err = h.q.CreateGame(c.Request().Context(), db.CreateGameParams{ + GameType: gameType, + IsPublic: isPublic, + DisplayName: displayName, + DurationSeconds: int32(durationSeconds), + ProblemID: int32(problemID), + }) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + return c.Redirect(http.StatusSeeOther, h.conf.BasePath+"admin/games") +} + func (h *Handler) getGameEdit(c echo.Context) error { gameID, err := strconv.Atoi(c.Param("gameID")) if err != nil { @@ -248,6 +342,20 @@ func (h *Handler) getGameEdit(c echo.Context) error { mainPlayer2 = int(mainPlayerRows[1].UserID) } + problemRows, err := h.q.ListProblems(c.Request().Context()) + if err != nil { + if !errors.Is(err, pgx.ErrNoRows) { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + } + var problems []echo.Map + for _, p := range problemRows { + problems = append(problems, echo.Map{ + "ProblemID": int(p.ProblemID), + "Title": p.Title, + }) + } + userRows, err := h.q.ListUsers(c.Request().Context()) if err != nil { if !errors.Is(err, pgx.ErrNoRows) { @@ -276,7 +384,8 @@ func (h *Handler) getGameEdit(c echo.Context) error { "MainPlayer1": mainPlayer1, "MainPlayer2": mainPlayer2, }, - "Users": users, + "Problems": problems, + "Users": users, }) } @@ -398,44 +507,6 @@ func (h *Handler) postGameStart(c echo.Context) error { return c.Redirect(http.StatusSeeOther, h.conf.BasePath+"admin/games") } -func (h *Handler) getOnlineQualifyingRanking(c echo.Context) error { - game1, err := strconv.Atoi(c.QueryParam("game_1")) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid game_1") - } - game2, err := strconv.Atoi(c.QueryParam("game_2")) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid game_2") - } - - rows, err := h.q.GetQualifyingRanking(c.Request().Context(), db.GetQualifyingRankingParams{ - GameID: int32(game1), - GameID_2: int32(game2), - }) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - - entries := make([]echo.Map, len(rows)) - for i, r := range rows { - entries[i] = echo.Map{ - "Rank": i + 1, - "Username": r.Username, - "UserLabel": r.UserLabel, - "Score1": r.CodeSize1, - "Score2": r.CodeSize2, - "TotalScore": r.TotalCodeSize, - "SubmittedAt1": r.SubmittedAt1.Time.In(jst).Format("2006-01-02T15:04"), - "SubmittedAt2": r.SubmittedAt2.Time.In(jst).Format("2006-01-02T15:04"), - } - } - return c.Render(http.StatusOK, "online_qualifying_ranking", echo.Map{ - "BasePath": h.conf.BasePath, - "Title": "Online Qualifying Ranking", - "Entries": entries, - }) -} - func (h *Handler) getProblems(c echo.Context) error { rows, err := h.q.ListProblems(c.Request().Context()) if err != nil { diff --git a/backend/admin/templates/game_edit.html b/backend/admin/templates/game_edit.html index 2d769c4..b171343 100644 --- a/backend/admin/templates/game_edit.html +++ b/backend/admin/templates/game_edit.html @@ -34,8 +34,12 @@ <input type="datetime-local" name="started_at" value="{{ if .Game.StartedAt }}{{ .Game.StartedAt }}{{ end }}"> </div> <div> - <label>Problem ID</label> - <input type="text" name="problem_id" value="{{ .Game.ProblemID }}"> + <label>Problem</label> + <select name="problem_id" required> + {{ range .Problems }} + <option value="{{ .ProblemID }}"{{ if eq $.Game.ProblemID .ProblemID }} selected{{ end }}>{{ .Title }} (id={{ .ProblemID }})</option> + {{ end }} + </select> </div> <div> <label>Main Player 1</label> diff --git a/backend/admin/templates/game_new.html b/backend/admin/templates/game_new.html new file mode 100644 index 0000000..3e3210a --- /dev/null +++ b/backend/admin/templates/game_new.html @@ -0,0 +1,40 @@ +{{ template "base.html" . }} + +{{ define "breadcrumb" }} +<a href="{{ .BasePath }}admin/dashboard">Dashboard</a> | <a href="{{ .BasePath }}admin/games">Games</a> +{{ end }} + +{{ define "content" }} +<form method="post"> + <div> + <label>Display Name</label> + <input type="text" name="display_name" required> + </div> + <div> + <label>Game Type</label> + <select name="game_type" required> + <option value="1v1">1v1</option> + <option value="multiplayer">Multiplayer</option> + </select> + </div> + <div> + <label>Is Public</label> + <input type="checkbox" name="is_public"> + </div> + <div> + <label>Duration Seconds</label> + <input type="number" name="duration_seconds" value="900" required> + </div> + <div> + <label>Problem</label> + <select name="problem_id" required> + {{ range .Problems }} + <option value="{{ .ProblemID }}">{{ .Title }} (id={{ .ProblemID }})</option> + {{ end }} + </select> + </div> + <div> + <button type="submit">Create</button> + </div> +</form> +{{ end }} diff --git a/backend/admin/templates/games.html b/backend/admin/templates/games.html index b5c512a..402c702 100644 --- a/backend/admin/templates/games.html +++ b/backend/admin/templates/games.html @@ -5,6 +5,9 @@ {{ end }} {{ define "content" }} +<div> + <a href="{{ .BasePath }}admin/games/new">Create New Game</a> +</div> <ul> {{ range .Games }} <li> diff --git a/backend/db/query.sql.go b/backend/db/query.sql.go index 3f85f46..001933a 100644 --- a/backend/db/query.sql.go +++ b/backend/db/query.sql.go @@ -50,6 +50,33 @@ func (q *Queries) AggregateTestcaseResults(ctx context.Context, submissionID int return status, err } +const createGame = `-- name: CreateGame :one +INSERT INTO games (game_type, is_public, display_name, duration_seconds, problem_id) +VALUES ($1, $2, $3, $4, $5) +RETURNING game_id +` + +type CreateGameParams struct { + GameType string + IsPublic bool + DisplayName string + DurationSeconds int32 + ProblemID int32 +} + +func (q *Queries) CreateGame(ctx context.Context, arg CreateGameParams) (int32, error) { + row := q.db.QueryRow(ctx, createGame, + arg.GameType, + arg.IsPublic, + arg.DisplayName, + arg.DurationSeconds, + arg.ProblemID, + ) + var game_id int32 + err := row.Scan(&game_id) + return game_id, err +} + const createProblem = `-- name: CreateProblem :one INSERT INTO problems (title, description, language, sample_code) VALUES ($1, $2, $3, $4) diff --git a/backend/query.sql b/backend/query.sql index 9f272b1..4d95e3f 100644 --- a/backend/query.sql +++ b/backend/query.sql @@ -51,6 +51,11 @@ ORDER BY games.game_id; SELECT * FROM games ORDER BY games.game_id; +-- name: CreateGame :one +INSERT INTO games (game_type, is_public, display_name, duration_seconds, problem_id) +VALUES ($1, $2, $3, $4, $5) +RETURNING game_id; + -- name: UpdateGameStartedAt :exec UPDATE games SET started_at = $2 |
