diff --git a/db/persister.go b/db/persister.go index d69bdf6..3894420 100644 --- a/db/persister.go +++ b/db/persister.go @@ -19,7 +19,7 @@ func NewGamePersister(q *gen.Queries) *GamePersister { func (p *GamePersister) SaveGame(g *game.Game) error { ctx := context.Background() - existing, err := p.queries.GetGame(ctx, g.ID) + _, err := p.queries.GetGame(ctx, g.ID) if err == sql.ErrNoRows { _, err = p.queries.CreateGame(ctx, gen.CreateGameParams{ ID: g.ID, @@ -43,7 +43,6 @@ func (p *GamePersister) SaveGame(g *game.Game) error { winningCells = sql.NullString{String: wc, Valid: true} } - _ = existing return p.queries.UpdateGame(ctx, gen.UpdateGameParams{ Board: g.BoardToJSON(), CurrentTurn: int64(g.CurrentTurn), @@ -124,3 +123,8 @@ func (p *GamePersister) LoadGamePlayers(gameID string) ([]*game.Player, error) { return players, nil } + +func (p *GamePersister) DeleteGame(id string) error { + ctx := context.Background() + return p.queries.DeleteGame(ctx, id) +} diff --git a/game/store.go b/game/store.go index 839ffce..a173385 100644 --- a/game/store.go +++ b/game/store.go @@ -21,6 +21,7 @@ type Persister interface { LoadGame(id string) (*Game, error) SaveGamePlayer(gameID string, player *Player, slot int) error LoadGamePlayers(gameID string) ([]*Player, error) + DeleteGame(id string) error } type GameStore struct { @@ -99,6 +100,21 @@ func (gs *GameStore) Get(id string) (*GameInstance, bool) { return gi, true } +func (gs *GameStore) Delete(id string) error { + gs.gamesMu.Lock() + gi, ok := gs.games[id] + if ok { + delete(gs.games, id) + close(gi.done) + } + gs.gamesMu.Unlock() + + if gs.persister != nil { + return gs.persister.DeleteGame(id) + } + return nil +} + func GenerateID(size int) string { b := make([]byte, size) rand.Read(b) diff --git a/main.go b/main.go index b4cd36b..e88358c 100644 --- a/main.go +++ b/main.go @@ -82,6 +82,18 @@ func main() { c.Redirectf("/game/%s", gi.ID()) }) + deleteGame := func(id string) h.H { + return c.Action(func() { + for _, g := range userGames { + if g.ID == id { + store.Delete(id) + break + } + } + c.Redirect("/") + }).OnClick() + } + c.View(func() h.H { return ui.LobbyView( nickname.Bind(), @@ -91,6 +103,7 @@ func main() { username, logout.OnClick(), userGames, + deleteGame, ) }) }) @@ -527,13 +540,11 @@ const gameCSS = ` .game-entry { display: flex; - justify-content: space-between; align-items: center; - padding: 0.75rem 1rem; + gap: 0.5rem; + padding: 0.5rem; background: var(--pico-muted-background); border-radius: 8px; - text-decoration: none; - color: inherit; transition: background 0.2s; } @@ -541,12 +552,41 @@ const gameCSS = ` background: var(--pico-secondary-background); } + .game-entry-link { + flex: 1; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.25rem 0.5rem; + text-decoration: none; + color: inherit; + } + .game-entry-main { display: flex; flex-direction: column; gap: 0.25rem; } + .game-delete-btn { + width: 2rem; + height: 2rem; + padding: 0; + margin: 0; + border: none; + background: transparent; + color: var(--pico-muted-color); + font-size: 1.25rem; + cursor: pointer; + border-radius: 4px; + transition: background 0.2s, color 0.2s; + } + + .game-delete-btn:hover { + background: #dc2626; + color: white; + } + .opponent-name { font-weight: bold; } diff --git a/ui/gamelist.go b/ui/gamelist.go index 5732bf9..2b305f3 100644 --- a/ui/gamelist.go +++ b/ui/gamelist.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/ryanhamamura/c4/game" "github.com/ryanhamamura/via/h" ) @@ -15,14 +16,14 @@ type GameListItem struct { LastPlayed time.Time } -func GameList(games []GameListItem) h.H { +func GameList(games []GameListItem, deleteClick func(id string) h.H) h.H { if len(games) == 0 { return nil } var items []h.H for _, g := range games { - items = append(items, gameListEntry(g)) + items = append(items, gameListEntry(g, deleteClick)) } listItems := []h.H{h.Class("game-list-items")} @@ -34,27 +35,35 @@ func GameList(games []GameListItem) h.H { ) } -func gameListEntry(g GameListItem) h.H { +func gameListEntry(g GameListItem, deleteClick func(id string) h.H) 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)), + return h.Div(h.Class("game-entry"), + h.A( + h.Href("/game/"+g.ID), + h.Class("game-entry-link"), + 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))), + ), ), - h.Div(h.Class("game-entry-meta"), - h.Span(h.Class("time-ago"), h.Text(formatTimeAgo(g.LastPlayed))), + h.Button( + h.Type("button"), + h.Class("game-delete-btn"), + h.Text("\u00d7"), + deleteClick(g.ID), ), ) } func getStatusDisplay(g GameListItem) (string, string) { - switch g.Status { - case 0: // Waiting + switch game.GameStatus(g.Status) { + case game.StatusWaitingForPlayer: return "Waiting for opponent", "waiting" - case 1: // In progress + case game.StatusInProgress: if g.IsMyTurn { return "Your turn!", "your-turn" } diff --git a/ui/lobby.go b/ui/lobby.go index 484ed3d..8f121f5 100644 --- a/ui/lobby.go +++ b/ui/lobby.go @@ -4,7 +4,7 @@ import ( "github.com/ryanhamamura/via/h" ) -func LobbyView(nicknameBind, createGameKeyDown, createGameClick h.H, isLoggedIn bool, username string, logoutClick h.H, userGames []GameListItem) h.H { +func LobbyView(nicknameBind, createGameKeyDown, createGameClick h.H, isLoggedIn bool, username string, logoutClick h.H, userGames []GameListItem, deleteGameClick func(id string) h.H) h.H { var authSection h.H if isLoggedIn { authSection = AuthHeader(username, logoutClick) @@ -35,7 +35,7 @@ func LobbyView(nicknameBind, createGameKeyDown, createGameClick h.H, isLoggedIn createGameClick, ), ), - GameList(userGames), + GameList(userGames, deleteGameClick), ), ) }