diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-03-11 01:23:41 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-03-11 01:23:41 +0900 |
| commit | a93c441ec4f992ed2f7eeae23fb823b1d152913f (patch) | |
| tree | c8e7750019849a7100d11ac6b81517ad0d5c98ea | |
| parent | 57212d2c7992a46827f503ae70c8e31f2084b718 (diff) | |
| download | iosdc-japan-2025-albatross-a93c441ec4f992ed2f7eeae23fb823b1d152913f.tar.gz iosdc-japan-2025-albatross-a93c441ec4f992ed2f7eeae23fb823b1d152913f.tar.zst iosdc-japan-2025-albatross-a93c441ec4f992ed2f7eeae23fb823b1d152913f.zip | |
feat: show user label
| -rw-r--r-- | backend/admin/handler.go | 1 | ||||
| -rw-r--r-- | backend/admin/templates/user_edit.html | 4 | ||||
| -rw-r--r-- | backend/api/generated.go | 58 | ||||
| -rw-r--r-- | backend/api/handler.go | 21 | ||||
| -rw-r--r-- | backend/db/models.go | 1 | ||||
| -rw-r--r-- | backend/db/query.sql.go | 18 | ||||
| -rw-r--r-- | backend/schema.sql | 1 | ||||
| -rw-r--r-- | frontend/app/api/schema.d.ts | 2 | ||||
| -rw-r--r-- | frontend/app/components/GolfWatchApps/GolfWatchAppGamingMultiplayer.tsx | 4 | ||||
| -rw-r--r-- | frontend/app/components/UserLabel.tsx | 11 | ||||
| -rw-r--r-- | openapi/api-server.yaml | 5 |
11 files changed, 93 insertions, 33 deletions
diff --git a/backend/admin/handler.go b/backend/admin/handler.go index 3e185ea..64f69d8 100644 --- a/backend/admin/handler.go +++ b/backend/admin/handler.go @@ -123,6 +123,7 @@ func (h *Handler) getUserEdit(c echo.Context) error { "DisplayName": row.DisplayName, "IconPath": row.IconPath, "IsAdmin": row.IsAdmin, + "Label": row.Label, }, }) } diff --git a/backend/admin/templates/user_edit.html b/backend/admin/templates/user_edit.html index d31338a..54b823b 100644 --- a/backend/admin/templates/user_edit.html +++ b/backend/admin/templates/user_edit.html @@ -27,6 +27,10 @@ <input type="checkbox" name="is_admin"{{ if .User.IsAdmin }} checked{{ end }}> </div> <div> + <label>Label</label> + <input type="text" name="label" value="{{ .User.Label }}"> + </div> + <div> <button type="submit">Save</button> </div> </form> diff --git a/backend/api/generated.go b/backend/api/generated.go index 001b264..a297c97 100644 --- a/backend/api/generated.go +++ b/backend/api/generated.go @@ -85,11 +85,12 @@ type RankingEntry struct { // User defines model for User. type User struct { - DisplayName string `json:"display_name"` - IconPath *string `json:"icon_path,omitempty"` - IsAdmin bool `json:"is_admin"` - UserID int `json:"user_id"` - Username string `json:"username"` + DisplayName string `json:"display_name"` + IconPath *string `json:"icon_path,omitempty"` + IsAdmin bool `json:"is_admin"` + Label nullable.Nullable[string] `json:"label"` + UserID int `json:"user_id"` + Username string `json:"username"` } // HeaderAuthorization defines model for header_authorization. @@ -1126,29 +1127,30 @@ func (sh *strictHandler) PostLogin(ctx echo.Context) error { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "HQAA//+TXFqx2R0AAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/backend/api/handler.go b/backend/api/handler.go index 4896a28..8d04693 100644 --- a/backend/api/handler.go +++ b/backend/api/handler.go @@ -101,12 +101,19 @@ func (h *Handler) GetGames(ctx context.Context, _ GetGamesRequestObject, _ *auth for _, row := range mainPlayerRows { idx := gameID2Index[row.GameID] game := &games[idx] + var label nullable.Nullable[string] + if row.Label != nil { + label = nullable.NewNullableWithValue(*row.Label) + } else { + label = nullable.NewNullNullable[string]() + } game.MainPlayers = append(game.MainPlayers, User{ UserID: int(row.UserID), Username: row.Username, DisplayName: row.DisplayName, IconPath: row.IconPath, IsAdmin: row.IsAdmin, + Label: label, }) } return GetGames200JSONResponse{ @@ -145,12 +152,19 @@ func (h *Handler) GetGame(ctx context.Context, request GetGameRequestObject, _ * } mainPlayers := make([]User, len(mainPlayerRows)) for i, playerRow := range mainPlayerRows { + var label nullable.Nullable[string] + if playerRow.Label != nil { + label = nullable.NewNullableWithValue(*playerRow.Label) + } else { + label = nullable.NewNullNullable[string]() + } mainPlayers[i] = User{ UserID: int(playerRow.UserID), Username: playerRow.Username, DisplayName: playerRow.DisplayName, IconPath: playerRow.IconPath, IsAdmin: playerRow.IsAdmin, + Label: label, } } game := Game{ @@ -260,6 +274,12 @@ func (h *Handler) GetGameWatchRanking(ctx context.Context, request GetGameWatchR } ranking := make([]RankingEntry, len(rows)) for i, row := range rows { + var label nullable.Nullable[string] + if row.Label != nil { + label = nullable.NewNullableWithValue(*row.Label) + } else { + label = nullable.NewNullNullable[string]() + } ranking[i] = RankingEntry{ Player: User{ UserID: int(row.UserID), @@ -267,6 +287,7 @@ func (h *Handler) GetGameWatchRanking(ctx context.Context, request GetGameWatchR DisplayName: row.DisplayName, IconPath: row.IconPath, IsAdmin: row.IsAdmin, + Label: label, }, Score: int(row.CodeSize), } diff --git a/backend/db/models.go b/backend/db/models.go index 18799b1..9bb8ccb 100644 --- a/backend/db/models.go +++ b/backend/db/models.go @@ -72,6 +72,7 @@ type User struct { DisplayName string IconPath *string IsAdmin bool + Label *string CreatedAt pgtype.Timestamp } diff --git a/backend/db/query.sql.go b/backend/db/query.sql.go index a7b8f74..5719e57 100644 --- a/backend/db/query.sql.go +++ b/backend/db/query.sql.go @@ -259,7 +259,7 @@ func (q *Queries) GetLatestStatesOfMainPlayers(ctx context.Context, gameID int32 } const getRanking = `-- name: GetRanking :many -SELECT game_states.game_id, game_states.user_id, game_states.code, game_states.status, best_score_submission_id, users.user_id, username, display_name, icon_path, is_admin, users.created_at, submission_id, submissions.game_id, submissions.user_id, submissions.code, code_size, submissions.status, submissions.created_at FROM game_states +SELECT game_states.game_id, game_states.user_id, game_states.code, game_states.status, best_score_submission_id, users.user_id, username, display_name, icon_path, is_admin, label, users.created_at, submission_id, submissions.game_id, submissions.user_id, submissions.code, code_size, submissions.status, submissions.created_at FROM game_states JOIN users ON game_states.user_id = users.user_id JOIN submissions ON game_states.best_score_submission_id = submissions.submission_id WHERE game_states.game_id = $1 @@ -277,6 +277,7 @@ type GetRankingRow struct { DisplayName string IconPath *string IsAdmin bool + Label *string CreatedAt pgtype.Timestamp SubmissionID int32 GameID_2 int32 @@ -307,6 +308,7 @@ func (q *Queries) GetRanking(ctx context.Context, gameID int32) ([]GetRankingRow &i.DisplayName, &i.IconPath, &i.IsAdmin, + &i.Label, &i.CreatedAt, &i.SubmissionID, &i.GameID_2, @@ -327,7 +329,7 @@ func (q *Queries) GetRanking(ctx context.Context, gameID int32) ([]GetRankingRow } const getUserAuthByUsername = `-- name: GetUserAuthByUsername :one -SELECT users.user_id, username, display_name, icon_path, is_admin, created_at, user_auth_id, user_auths.user_id, auth_type, password_hash FROM users +SELECT users.user_id, username, display_name, icon_path, is_admin, label, created_at, user_auth_id, user_auths.user_id, auth_type, password_hash FROM users JOIN user_auths ON users.user_id = user_auths.user_id WHERE users.username = $1 LIMIT 1 @@ -339,6 +341,7 @@ type GetUserAuthByUsernameRow struct { DisplayName string IconPath *string IsAdmin bool + Label *string CreatedAt pgtype.Timestamp UserAuthID int32 UserID_2 int32 @@ -355,6 +358,7 @@ func (q *Queries) GetUserAuthByUsername(ctx context.Context, username string) (G &i.DisplayName, &i.IconPath, &i.IsAdmin, + &i.Label, &i.CreatedAt, &i.UserAuthID, &i.UserID_2, @@ -365,7 +369,7 @@ func (q *Queries) GetUserAuthByUsername(ctx context.Context, username string) (G } const getUserByID = `-- name: GetUserByID :one -SELECT user_id, username, display_name, icon_path, is_admin, created_at FROM users +SELECT user_id, username, display_name, icon_path, is_admin, label, created_at FROM users WHERE users.user_id = $1 LIMIT 1 ` @@ -379,6 +383,7 @@ func (q *Queries) GetUserByID(ctx context.Context, userID int32) (User, error) { &i.DisplayName, &i.IconPath, &i.IsAdmin, + &i.Label, &i.CreatedAt, ) return i, err @@ -432,7 +437,7 @@ func (q *Queries) ListAllGames(ctx context.Context) ([]Game, error) { } const listMainPlayers = `-- name: ListMainPlayers :many -SELECT game_id, game_main_players.user_id, users.user_id, username, display_name, icon_path, is_admin, created_at FROM game_main_players +SELECT game_id, game_main_players.user_id, users.user_id, username, display_name, icon_path, is_admin, label, created_at FROM game_main_players JOIN users ON game_main_players.user_id = users.user_id WHERE game_main_players.game_id = ANY($1::INT[]) ORDER BY game_main_players.user_id @@ -446,6 +451,7 @@ type ListMainPlayersRow struct { DisplayName string IconPath *string IsAdmin bool + Label *string CreatedAt pgtype.Timestamp } @@ -466,6 +472,7 @@ func (q *Queries) ListMainPlayers(ctx context.Context, dollar_1 []int32) ([]List &i.DisplayName, &i.IconPath, &i.IsAdmin, + &i.Label, &i.CreatedAt, ); err != nil { return nil, err @@ -565,7 +572,7 @@ func (q *Queries) ListTestcasesByGameID(ctx context.Context, gameID int32) ([]Te } const listUsers = `-- name: ListUsers :many -SELECT user_id, username, display_name, icon_path, is_admin, created_at FROM users +SELECT user_id, username, display_name, icon_path, is_admin, label, created_at FROM users ORDER BY users.user_id ` @@ -584,6 +591,7 @@ func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { &i.DisplayName, &i.IconPath, &i.IsAdmin, + &i.Label, &i.CreatedAt, ); err != nil { return nil, err diff --git a/backend/schema.sql b/backend/schema.sql index 8c63ff2..6550fcd 100644 --- a/backend/schema.sql +++ b/backend/schema.sql @@ -4,6 +4,7 @@ CREATE TABLE users ( display_name VARCHAR(64) NOT NULL, icon_path VARCHAR(255), is_admin BOOLEAN NOT NULL, + label VARCHAR(16), created_at TIMESTAMP NOT NULL DEFAULT NOW() ); CREATE INDEX idx_users_username ON users(username); diff --git a/frontend/app/api/schema.d.ts b/frontend/app/api/schema.d.ts index cec5661..b5ec26b 100644 --- a/frontend/app/api/schema.d.ts +++ b/frontend/app/api/schema.d.ts @@ -159,6 +159,8 @@ export interface components { icon_path?: string; /** @example false */ is_admin: boolean; + /** @example staff */ + label: string | null; }; Game: { /** @example 1 */ diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGamingMultiplayer.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGamingMultiplayer.tsx index 7a36283..758c589 100644 --- a/frontend/app/components/GolfWatchApps/GolfWatchAppGamingMultiplayer.tsx +++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGamingMultiplayer.tsx @@ -2,6 +2,7 @@ import { useAtomValue } from "jotai"; import type { components } from "../../api/schema"; import { gamingLeftTimeSecondsAtom } from "../../states/watch"; import BorderedContainer from "../BorderedContainer"; +import UserLabel from "../UserLabel"; type RankingEntry = components["schemas"]["RankingEntry"]; @@ -91,6 +92,9 @@ export default function GolfWatchAppGamingMultiplayer({ </td> <td className="px-6 py-4 whitespace-nowrap text-gray-900"> {entry.player.display_name} + {entry.player.label && ( + <UserLabel label={entry.player.label} /> + )} </td> <td className="px-6 py-4 whitespace-nowrap text-gray-900"> {entry.score} diff --git a/frontend/app/components/UserLabel.tsx b/frontend/app/components/UserLabel.tsx new file mode 100644 index 0000000..b436ad6 --- /dev/null +++ b/frontend/app/components/UserLabel.tsx @@ -0,0 +1,11 @@ +type Props = { + label: string; +}; + +export default function UserLabel({ label }: Props) { + return ( + <span className="bg-sky-700 text-sky-50 rounded-lg p-3 text-sm"> + {label} + </span> + ); +} diff --git a/openapi/api-server.yaml b/openapi/api-server.yaml index 10f4d7e..9b43bc9 100644 --- a/openapi/api-server.yaml +++ b/openapi/api-server.yaml @@ -288,11 +288,16 @@ components: is_admin: type: boolean example: false + label: + type: string + nullable: true + example: "staff" required: - user_id - username - display_name - is_admin + - label Game: type: object properties: |
