aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-09-06 01:09:20 +0900
committernsfisis <nsfisis@gmail.com>2025-09-06 01:09:20 +0900
commite7b35cd81f2e515371a30a59ea4173a31dbeefc5 (patch)
tree783ef649110730a8946b0021c50688d29b6dab9b
parente33bfff4db95586a3140b5e71a7d3dba2c72f694 (diff)
downloadiosdc-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.go149
-rw-r--r--backend/admin/templates/game_edit.html8
-rw-r--r--backend/admin/templates/game_new.html40
-rw-r--r--backend/admin/templates/games.html3
-rw-r--r--backend/db/query.sql.go27
-rw-r--r--backend/query.sql5
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