refactor: replace via framework with chi + templ + datastar
Migrate from the via meta-framework to direct dependencies:
- chi for routing, templ for HTML templates, datastar for SSE/reactivity
- Feature-sliced architecture (features/{auth,lobby,c4game,snakegame}/)
- Shared layouts and components (features/common/)
- Handler factory pattern (HandleX(deps) http.HandlerFunc)
- Embedded NATS server (nats/), SCS sessions (sessions/), chi router wiring (router/)
- Move ChatMessage domain type from ui package to game package
- Remove old ui/ package (gomponents-based via/h views)
- Remove via dependency from go.mod entirely
This commit is contained in:
168
features/lobby/handlers.go
Normal file
168
features/lobby/handlers.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package lobby
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/ryanhamamura/c4/db/repository"
|
||||
lobbycomponents "github.com/ryanhamamura/c4/features/lobby/components"
|
||||
"github.com/ryanhamamura/c4/features/lobby/pages"
|
||||
"github.com/ryanhamamura/c4/game"
|
||||
"github.com/ryanhamamura/c4/snake"
|
||||
|
||||
"github.com/alexedwards/scs/v2"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/starfederation/datastar-go/datastar"
|
||||
)
|
||||
|
||||
// HandleLobbyPage renders the main lobby page with active games for logged-in users.
|
||||
func HandleLobbyPage(queries *repository.Queries, sessions *scs.SessionManager, snakeStore *snake.SnakeStore) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID := sessions.GetString(r.Context(), "user_id")
|
||||
username := sessions.GetString(r.Context(), "username")
|
||||
isLoggedIn := userID != ""
|
||||
|
||||
var userGames []lobbycomponents.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, lobbycomponents.GameListItem{
|
||||
ID: g.ID,
|
||||
Status: int(g.Status),
|
||||
OpponentName: g.OpponentNickname.String,
|
||||
IsMyTurn: isMyTurn,
|
||||
LastPlayed: g.UpdatedAt.Time,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var activeSnakeGames []pages.SnakeGameListItem
|
||||
for _, g := range snakeStore.ActiveGames() {
|
||||
statusLabel := "Waiting"
|
||||
if g.Status == snake.StatusCountdown {
|
||||
statusLabel = "Starting soon"
|
||||
}
|
||||
activeSnakeGames = append(activeSnakeGames, pages.SnakeGameListItem{
|
||||
ID: g.ID,
|
||||
Width: g.State.Width,
|
||||
Height: g.State.Height,
|
||||
PlayerCount: g.PlayerCount(),
|
||||
StatusLabel: statusLabel,
|
||||
})
|
||||
}
|
||||
|
||||
data := pages.LobbyData{
|
||||
IsLoggedIn: isLoggedIn,
|
||||
Username: username,
|
||||
UserGames: userGames,
|
||||
ActiveSnakeGames: activeSnakeGames,
|
||||
}
|
||||
|
||||
if err := pages.LobbyPage(data).Render(r.Context(), w); err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleCreateGame reads the nickname signal, creates a connect4 game, and redirects via SSE.
|
||||
func HandleCreateGame(store *game.GameStore, sessions *scs.SessionManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
type Signals struct {
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
signals := &Signals{}
|
||||
if err := datastar.ReadSignals(r, signals); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if signals.Nickname == "" {
|
||||
return
|
||||
}
|
||||
|
||||
sessions.Put(r.Context(), "nickname", signals.Nickname)
|
||||
|
||||
gi := store.Create()
|
||||
sse := datastar.NewSSE(w, r)
|
||||
sse.ExecuteScript(fmt.Sprintf("window.location.href='/game/%s'", gi.ID()))
|
||||
}
|
||||
}
|
||||
|
||||
// HandleDeleteGame deletes a connect4 game and redirects to the lobby.
|
||||
func HandleDeleteGame(store *game.GameStore, sessions *scs.SessionManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
gameID := chi.URLParam(r, "id")
|
||||
if gameID == "" {
|
||||
http.Error(w, "missing game id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
store.Delete(gameID)
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
sse.ExecuteScript("window.location.href='/'")
|
||||
}
|
||||
}
|
||||
|
||||
// HandleCreateSnakeGame reads nickname, grid preset, speed, and mode from the request,
|
||||
// creates a snake game, and redirects via SSE.
|
||||
func HandleCreateSnakeGame(snakeStore *snake.SnakeStore, sessions *scs.SessionManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
type Signals struct {
|
||||
Nickname string `json:"nickname"`
|
||||
SelectedSpeed int `json:"selectedSpeed"`
|
||||
}
|
||||
signals := &Signals{}
|
||||
if err := datastar.ReadSignals(r, signals); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if signals.Nickname == "" {
|
||||
return
|
||||
}
|
||||
|
||||
sessions.Put(r.Context(), "nickname", signals.Nickname)
|
||||
|
||||
mode := snake.ModeMultiplayer
|
||||
if r.URL.Query().Get("mode") == "solo" {
|
||||
mode = snake.ModeSinglePlayer
|
||||
}
|
||||
|
||||
presetIdx, _ := strconv.Atoi(r.URL.Query().Get("preset"))
|
||||
if presetIdx < 0 || presetIdx >= len(snake.GridPresets) {
|
||||
presetIdx = 0
|
||||
}
|
||||
preset := snake.GridPresets[presetIdx]
|
||||
|
||||
speed := snake.DefaultSpeed
|
||||
if signals.SelectedSpeed >= 0 && signals.SelectedSpeed < len(snake.SpeedPresets) {
|
||||
speed = snake.SpeedPresets[signals.SelectedSpeed].Speed
|
||||
}
|
||||
|
||||
si := snakeStore.Create(preset.Width, preset.Height, mode, speed)
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
sse.ExecuteScript(fmt.Sprintf("window.location.href='/snake/%s'", si.ID()))
|
||||
}
|
||||
}
|
||||
|
||||
// HandleLogout clears the session and redirects to the lobby.
|
||||
func HandleLogout(sessions *scs.SessionManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := sessions.Destroy(r.Context()); err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
sse.ExecuteScript("window.location.href='/'")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user