Show user's active games on home page after login
This commit is contained in:
@@ -209,6 +209,60 @@ func (q *Queries) GetGamesByUserID(ctx context.Context, userID sql.NullString) (
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getUserActiveGames = `-- name: GetUserActiveGames :many
|
||||
SELECT
|
||||
g.id,
|
||||
g.status,
|
||||
g.current_turn,
|
||||
g.updated_at,
|
||||
gp_user.color as my_color,
|
||||
gp_opponent.nickname as opponent_nickname
|
||||
FROM games g
|
||||
JOIN game_players gp_user ON g.id = gp_user.game_id AND gp_user.user_id = ?
|
||||
LEFT JOIN game_players gp_opponent ON g.id = gp_opponent.game_id AND gp_opponent.slot != gp_user.slot
|
||||
WHERE g.status < 2
|
||||
ORDER BY g.updated_at DESC
|
||||
`
|
||||
|
||||
type GetUserActiveGamesRow struct {
|
||||
ID string
|
||||
Status int64
|
||||
CurrentTurn int64
|
||||
UpdatedAt sql.NullTime
|
||||
MyColor int64
|
||||
OpponentNickname sql.NullString
|
||||
}
|
||||
|
||||
func (q *Queries) GetUserActiveGames(ctx context.Context, userID sql.NullString) ([]GetUserActiveGamesRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getUserActiveGames, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetUserActiveGamesRow
|
||||
for rows.Next() {
|
||||
var i GetUserActiveGamesRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Status,
|
||||
&i.CurrentTurn,
|
||||
&i.UpdatedAt,
|
||||
&i.MyColor,
|
||||
&i.OpponentNickname,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateGame = `-- name: UpdateGame :exec
|
||||
UPDATE games
|
||||
SET board = ?, current_turn = ?, status = ?, winner_user_id = ?, winning_cells = ?, updated_at = CURRENT_TIMESTAMP
|
||||
|
||||
@@ -29,3 +29,17 @@ SELECT g.* FROM games g
|
||||
JOIN game_players gp ON g.id = gp.game_id
|
||||
WHERE gp.user_id = ?
|
||||
ORDER BY g.updated_at DESC;
|
||||
|
||||
-- name: GetUserActiveGames :many
|
||||
SELECT
|
||||
g.id,
|
||||
g.status,
|
||||
g.current_turn,
|
||||
g.updated_at,
|
||||
gp_user.color as my_color,
|
||||
gp_opponent.nickname as opponent_nickname
|
||||
FROM games g
|
||||
JOIN game_players gp_user ON g.id = gp_user.game_id AND gp_user.user_id = ?
|
||||
LEFT JOIN game_players gp_opponent ON g.id = gp_opponent.game_id AND gp_opponent.slot != gp_user.slot
|
||||
WHERE g.status < 2
|
||||
ORDER BY g.updated_at DESC;
|
||||
|
||||
79
main.go
79
main.go
@@ -43,6 +43,24 @@ func main() {
|
||||
username := c.Session().GetString("username")
|
||||
isLoggedIn := userID != ""
|
||||
|
||||
var userGames []ui.GameListItem
|
||||
if isLoggedIn {
|
||||
ctx := context.Background()
|
||||
games, err := queries.GetUserActiveGames(ctx, sql.NullString{String: userID, Valid: true})
|
||||
if err == nil {
|
||||
for _, g := range games {
|
||||
isMyTurn := g.Status == 1 && g.CurrentTurn == g.MyColor
|
||||
userGames = append(userGames, ui.GameListItem{
|
||||
ID: g.ID,
|
||||
Status: int(g.Status),
|
||||
OpponentName: g.OpponentNickname.String,
|
||||
IsMyTurn: isMyTurn,
|
||||
LastPlayed: g.UpdatedAt.Time,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nickname := c.Signal("")
|
||||
if isLoggedIn {
|
||||
nickname = c.Signal(username)
|
||||
@@ -72,6 +90,7 @@ func main() {
|
||||
isLoggedIn,
|
||||
username,
|
||||
logout.OnClick(),
|
||||
userGames,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -489,4 +508,64 @@ const gameCSS = `
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.game-list {
|
||||
margin-top: 2rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.game-list h3 {
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.game-list-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.game-entry {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--pico-muted-background);
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.game-entry:hover {
|
||||
background: var(--pico-secondary-background);
|
||||
}
|
||||
|
||||
.game-entry-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.opponent-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.game-status {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.game-status.your-turn {
|
||||
color: #22c55e;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.game-status.waiting {
|
||||
color: var(--pico-muted-color);
|
||||
}
|
||||
|
||||
.time-ago {
|
||||
font-size: 0.75rem;
|
||||
color: var(--pico-muted-color);
|
||||
}
|
||||
`
|
||||
|
||||
101
ui/gamelist.go
Normal file
101
ui/gamelist.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ryanhamamura/via/h"
|
||||
)
|
||||
|
||||
type GameListItem struct {
|
||||
ID string
|
||||
Status int
|
||||
OpponentName string
|
||||
IsMyTurn bool
|
||||
LastPlayed time.Time
|
||||
}
|
||||
|
||||
func GameList(games []GameListItem) h.H {
|
||||
if len(games) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var items []h.H
|
||||
for _, g := range games {
|
||||
items = append(items, gameListEntry(g))
|
||||
}
|
||||
|
||||
listItems := []h.H{h.Class("game-list-items")}
|
||||
listItems = append(listItems, items...)
|
||||
|
||||
return h.Div(h.Class("game-list"),
|
||||
h.H3(h.Text("Your Games")),
|
||||
h.Div(listItems...),
|
||||
)
|
||||
}
|
||||
|
||||
func gameListEntry(g GameListItem) h.H {
|
||||
statusText, statusClass := getStatusDisplay(g)
|
||||
|
||||
return h.A(
|
||||
h.Href("/game/"+g.ID),
|
||||
h.Class("game-entry"),
|
||||
h.Div(h.Class("game-entry-main"),
|
||||
h.Span(h.Class("opponent-name"), h.Text(getOpponentDisplay(g))),
|
||||
h.Span(h.Class("game-status "+statusClass), h.Text(statusText)),
|
||||
),
|
||||
h.Div(h.Class("game-entry-meta"),
|
||||
h.Span(h.Class("time-ago"), h.Text(formatTimeAgo(g.LastPlayed))),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func getStatusDisplay(g GameListItem) (string, string) {
|
||||
switch g.Status {
|
||||
case 0: // Waiting
|
||||
return "Waiting for opponent", "waiting"
|
||||
case 1: // In progress
|
||||
if g.IsMyTurn {
|
||||
return "Your turn!", "your-turn"
|
||||
}
|
||||
return "Opponent's turn", "opponent-turn"
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func getOpponentDisplay(g GameListItem) string {
|
||||
if g.OpponentName == "" {
|
||||
return "Waiting for opponent..."
|
||||
}
|
||||
return "vs " + g.OpponentName
|
||||
}
|
||||
|
||||
func formatTimeAgo(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
duration := time.Since(t)
|
||||
|
||||
if duration < time.Minute {
|
||||
return "just now"
|
||||
}
|
||||
if duration < time.Hour {
|
||||
mins := int(duration.Minutes())
|
||||
if mins == 1 {
|
||||
return "1 minute ago"
|
||||
}
|
||||
return fmt.Sprintf("%d minutes ago", mins)
|
||||
}
|
||||
if duration < 24*time.Hour {
|
||||
hours := int(duration.Hours())
|
||||
if hours == 1 {
|
||||
return "1 hour ago"
|
||||
}
|
||||
return fmt.Sprintf("%d hours ago", hours)
|
||||
}
|
||||
days := int(duration.Hours() / 24)
|
||||
if days == 1 {
|
||||
return "yesterday"
|
||||
}
|
||||
return fmt.Sprintf("%d days ago", days)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"github.com/ryanhamamura/via/h"
|
||||
)
|
||||
|
||||
func LobbyView(nicknameBind, createGameKeyDown, createGameClick h.H, isLoggedIn bool, username string, logoutClick h.H) h.H {
|
||||
func LobbyView(nicknameBind, createGameKeyDown, createGameClick h.H, isLoggedIn bool, username string, logoutClick h.H, userGames []GameListItem) h.H {
|
||||
var authSection h.H
|
||||
if isLoggedIn {
|
||||
authSection = AuthHeader(username, logoutClick)
|
||||
@@ -35,6 +35,7 @@ func LobbyView(nicknameBind, createGameKeyDown, createGameClick h.H, isLoggedIn
|
||||
createGameClick,
|
||||
),
|
||||
),
|
||||
GameList(userGames),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user