Replace PicoCSS with DaisyUI + Tailwind v4

Use gotailwind (standalone Tailwind v4 via Go tool) with DaisyUI
plugin files — no npm needed. CSS is compiled at build time and
embedded via a Via Plugin that serves it as a static file.

Custom "connect4" theme: light, warm, playful palette with red/yellow
accents matching game pieces and board blue accent.
This commit is contained in:
Ryan Hamamura
2026-01-31 07:31:29 -10:00
parent dcab4343e5
commit f590a2444a
11 changed files with 2897 additions and 471 deletions

View File

@@ -7,109 +7,112 @@ import (
func LoginView(usernameBind, passwordBind, loginKeyDown, loginClick h.H, errorMsg string) h.H {
var errorEl h.H
if errorMsg != "" {
errorEl = h.P(h.Class("error"), h.Text(errorMsg))
errorEl = h.P(h.Class("alert alert-error mb-4"), h.Text(errorMsg))
}
return h.Main(h.Class("container"),
h.Div(h.Class("lobby"),
h.H1(h.Text("Login")),
h.P(h.Text("Sign in to your account")),
errorEl,
h.Form(
h.FieldSet(
h.Label(h.Text("Username"), h.Attr("for", "username")),
h.Input(
h.ID("username"),
h.Type("text"),
h.Placeholder("Enter your username"),
usernameBind,
h.Attr("required"),
h.Attr("autofocus"),
),
h.Label(h.Text("Password"), h.Attr("for", "password")),
h.Input(
h.ID("password"),
h.Type("password"),
h.Placeholder("Enter your password"),
passwordBind,
h.Attr("required"),
loginKeyDown,
),
return h.Main(h.Class("max-w-sm mx-auto mt-8 text-center"),
h.H1(h.Class("text-3xl font-bold"), h.Text("Login")),
h.P(h.Class("mb-4"), h.Text("Sign in to your account")),
errorEl,
h.Form(
h.FieldSet(h.Class("fieldset"),
h.Label(h.Class("label"), h.Text("Username"), h.Attr("for", "username")),
h.Input(
h.Class("input input-bordered w-full"),
h.ID("username"),
h.Type("text"),
h.Placeholder("Enter your username"),
usernameBind,
h.Attr("required"),
h.Attr("autofocus"),
),
h.Button(
h.Type("button"),
h.Text("Login"),
loginClick,
h.Label(h.Class("label"), h.Text("Password"), h.Attr("for", "password")),
h.Input(
h.Class("input input-bordered w-full"),
h.ID("password"),
h.Type("password"),
h.Placeholder("Enter your password"),
passwordBind,
h.Attr("required"),
loginKeyDown,
),
),
h.P(
h.Text("Don't have an account? "),
h.A(h.Href("/register"), h.Text("Register")),
h.Button(
h.Class("btn btn-primary w-full"),
h.Type("button"),
h.Text("Login"),
loginClick,
),
),
h.P(
h.Text("Don't have an account? "),
h.A(h.Class("link"), h.Href("/register"), h.Text("Register")),
),
)
}
func RegisterView(usernameBind, passwordBind, confirmBind, registerKeyDown, registerClick h.H, errorMsg string) h.H {
var errorEl h.H
if errorMsg != "" {
errorEl = h.P(h.Class("error"), h.Text(errorMsg))
errorEl = h.P(h.Class("alert alert-error mb-4"), h.Text(errorMsg))
}
return h.Main(h.Class("container"),
h.Div(h.Class("lobby"),
h.H1(h.Text("Register")),
h.P(h.Text("Create a new account")),
errorEl,
h.Form(
h.FieldSet(
h.Label(h.Text("Username"), h.Attr("for", "username")),
h.Input(
h.ID("username"),
h.Type("text"),
h.Placeholder("Choose a username"),
usernameBind,
h.Attr("required"),
h.Attr("autofocus"),
),
h.Label(h.Text("Password"), h.Attr("for", "password")),
h.Input(
h.ID("password"),
h.Type("password"),
h.Placeholder("Choose a password (min 8 chars)"),
passwordBind,
h.Attr("required"),
),
h.Label(h.Text("Confirm Password"), h.Attr("for", "confirm")),
h.Input(
h.ID("confirm"),
h.Type("password"),
h.Placeholder("Confirm your password"),
confirmBind,
h.Attr("required"),
registerKeyDown,
),
return h.Main(h.Class("max-w-sm mx-auto mt-8 text-center"),
h.H1(h.Class("text-3xl font-bold"), h.Text("Register")),
h.P(h.Class("mb-4"), h.Text("Create a new account")),
errorEl,
h.Form(
h.FieldSet(h.Class("fieldset"),
h.Label(h.Class("label"), h.Text("Username"), h.Attr("for", "username")),
h.Input(
h.Class("input input-bordered w-full"),
h.ID("username"),
h.Type("text"),
h.Placeholder("Choose a username"),
usernameBind,
h.Attr("required"),
h.Attr("autofocus"),
),
h.Button(
h.Type("button"),
h.Text("Register"),
registerClick,
h.Label(h.Class("label"), h.Text("Password"), h.Attr("for", "password")),
h.Input(
h.Class("input input-bordered w-full"),
h.ID("password"),
h.Type("password"),
h.Placeholder("Choose a password (min 8 chars)"),
passwordBind,
h.Attr("required"),
),
h.Label(h.Class("label"), h.Text("Confirm Password"), h.Attr("for", "confirm")),
h.Input(
h.Class("input input-bordered w-full"),
h.ID("confirm"),
h.Type("password"),
h.Placeholder("Confirm your password"),
confirmBind,
h.Attr("required"),
registerKeyDown,
),
),
h.P(
h.Text("Already have an account? "),
h.A(h.Href("/login"), h.Text("Login")),
h.Button(
h.Class("btn btn-primary w-full"),
h.Type("button"),
h.Text("Register"),
registerClick,
),
),
h.P(
h.Text("Already have an account? "),
h.A(h.Class("link"), h.Href("/login"), h.Text("Login")),
),
)
}
func AuthHeader(username string, logoutClick h.H) h.H {
return h.Div(h.Class("auth-header"),
return h.Div(h.Class("flex justify-center items-center gap-4 mb-4 p-2 bg-base-200 rounded-lg"),
h.Span(h.Text("Logged in as "), h.Strong(h.Text(username))),
h.Button(
h.Type("button"),
h.Class("secondary outline small"),
h.Class("btn btn-ghost btn-sm"),
h.Text("Logout"),
logoutClick,
),
@@ -117,11 +120,11 @@ func AuthHeader(username string, logoutClick h.H) h.H {
}
func GuestBanner() h.H {
return h.Div(h.Class("guest-banner"),
return h.Div(h.Class("alert text-sm mb-4"),
h.Text("Playing as guest. "),
h.A(h.Href("/login"), h.Text("Login")),
h.A(h.Class("link"), h.Href("/login"), h.Text("Login")),
h.Text(" or "),
h.A(h.Href("/register"), h.Text("Register")),
h.A(h.Class("link"), h.Href("/register"), h.Text("Register")),
h.Text(" to save your games."),
)
}

View File

@@ -26,11 +26,11 @@ func GameList(games []GameListItem, deleteClick func(id string) h.H) h.H {
items = append(items, gameListEntry(g, deleteClick))
}
listItems := []h.H{h.Class("game-list-items")}
listItems := []h.H{h.Class("flex flex-col gap-2")}
listItems = append(listItems, items...)
return h.Div(h.Class("game-list"),
h.H3(h.Text("Your Games")),
return h.Div(h.Class("mt-8 text-left"),
h.H3(h.Class("mb-4 text-center text-lg font-bold"), h.Text("Your Games")),
h.Div(listItems...),
)
}
@@ -38,21 +38,21 @@ func GameList(games []GameListItem, deleteClick func(id string) h.H) h.H {
func gameListEntry(g GameListItem, deleteClick func(id string) h.H) h.H {
statusText, statusClass := getStatusDisplay(g)
return h.Div(h.Class("game-entry"),
return h.Div(h.Class("flex items-center gap-2 p-2 bg-base-200 rounded-lg transition-colors hover:bg-base-300"),
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.Class("flex-1 flex justify-between items-center px-2 py-1 no-underline text-base-content"),
h.Div(h.Class("flex flex-col gap-1"),
h.Span(h.Class("font-bold"), h.Text(getOpponentDisplay(g))),
h.Span(h.Class(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.Span(h.Class("text-xs opacity-60"), h.Text(formatTimeAgo(g.LastPlayed))),
),
),
h.Button(
h.Type("button"),
h.Class("game-delete-btn"),
h.Class("btn btn-ghost btn-sm btn-square hover:btn-error"),
h.Text("\u00d7"),
deleteClick(g.ID),
),
@@ -62,12 +62,12 @@ func gameListEntry(g GameListItem, deleteClick func(id string) h.H) h.H {
func getStatusDisplay(g GameListItem) (string, string) {
switch game.GameStatus(g.Status) {
case game.StatusWaitingForPlayer:
return "Waiting for opponent", "waiting"
return "Waiting for opponent", "text-sm opacity-60"
case game.StatusInProgress:
if g.IsMyTurn {
return "Your turn!", "your-turn"
return "Your turn!", "text-sm text-success font-bold"
}
return "Opponent's turn", "opponent-turn"
return "Opponent's turn", "text-sm"
}
return "", ""
}

View File

@@ -12,84 +12,83 @@ func LobbyView(nicknameBind, createGameKeyDown, createGameClick h.H, isLoggedIn
authSection = GuestBanner()
}
return h.Main(h.Class("container"),
h.Div(h.Class("lobby"),
authSection,
h.H1(h.Text("Connect 4")),
h.P(h.Text("Challenge a friend to a game of Connect 4!")),
h.Form(
h.FieldSet(
h.Label(h.Text("Your Nickname"), h.Attr("for", "nickname")),
h.Input(
h.ID("nickname"),
h.Type("text"),
h.Placeholder("Enter your nickname"),
nicknameBind,
h.Attr("required"),
createGameKeyDown,
),
),
h.Button(
h.Type("button"),
h.Text("Create Game"),
createGameClick,
return h.Main(h.Class("max-w-sm mx-auto mt-8 text-center"),
authSection,
h.H1(h.Class("text-3xl font-bold"), h.Text("Connect 4")),
h.P(h.Class("mb-4"), h.Text("Challenge a friend to a game of Connect 4!")),
h.Form(
h.FieldSet(h.Class("fieldset"),
h.Label(h.Class("label"), h.Text("Your Nickname"), h.Attr("for", "nickname")),
h.Input(
h.Class("input input-bordered w-full"),
h.ID("nickname"),
h.Type("text"),
h.Placeholder("Enter your nickname"),
nicknameBind,
h.Attr("required"),
createGameKeyDown,
),
),
GameList(userGames, deleteGameClick),
h.Button(
h.Class("btn btn-primary w-full"),
h.Type("button"),
h.Text("Create Game"),
createGameClick,
),
),
GameList(userGames, deleteGameClick),
)
}
func NicknamePrompt(nicknameBind, setNicknameKeyDown, setNicknameClick h.H) h.H {
return h.Main(h.Class("container"),
h.Div(h.Class("lobby"),
h.H1(h.Text("Join Game")),
h.P(h.Text("Enter your nickname to join the game.")),
h.Form(
h.FieldSet(
h.Label(h.Text("Your Nickname"), h.Attr("for", "nickname")),
h.Input(
h.ID("nickname"),
h.Type("text"),
h.Placeholder("Enter your nickname"),
nicknameBind,
h.Attr("required"),
h.Attr("autofocus"),
setNicknameKeyDown,
),
),
h.Button(
h.Type("button"),
h.Text("Join"),
setNicknameClick,
return h.Main(h.Class("max-w-sm mx-auto mt-8 text-center"),
h.H1(h.Class("text-3xl font-bold"), h.Text("Join Game")),
h.P(h.Class("mb-4"), h.Text("Enter your nickname to join the game.")),
h.Form(
h.FieldSet(h.Class("fieldset"),
h.Label(h.Class("label"), h.Text("Your Nickname"), h.Attr("for", "nickname")),
h.Input(
h.Class("input input-bordered w-full"),
h.ID("nickname"),
h.Type("text"),
h.Placeholder("Enter your nickname"),
nicknameBind,
h.Attr("required"),
h.Attr("autofocus"),
setNicknameKeyDown,
),
),
h.Button(
h.Class("btn btn-primary w-full"),
h.Type("button"),
h.Text("Join"),
setNicknameClick,
),
),
)
}
func GameJoinPrompt(loginClick, guestClick, registerClick h.H) h.H {
return h.Main(h.Class("container"),
h.Div(h.Class("lobby"),
h.H1(h.Text("Join Game")),
h.P(h.Text("Log in to track your game history, or continue as a guest.")),
h.Div(h.Class("join-options"),
h.Button(
h.Type("button"),
h.Text("Login"),
loginClick,
),
h.Button(
h.Type("button"),
h.Class("secondary"),
h.Text("Continue as Guest"),
guestClick,
),
return h.Main(h.Class("max-w-sm mx-auto mt-8 text-center"),
h.H1(h.Class("text-3xl font-bold"), h.Text("Join Game")),
h.P(h.Class("mb-4"), h.Text("Log in to track your game history, or continue as a guest.")),
h.Div(h.Class("flex flex-col gap-2 my-4"),
h.Button(
h.Class("btn btn-primary w-full"),
h.Type("button"),
h.Text("Login"),
loginClick,
),
h.P(h.Class("register-link"),
h.Text("Don't have an account? "),
h.A(h.Href("#"), h.Text("Register"), registerClick),
h.Button(
h.Class("btn btn-secondary w-full"),
h.Type("button"),
h.Text("Continue as Guest"),
guestClick,
),
),
h.P(h.Class("text-sm opacity-60"),
h.Text("Don't have an account? "),
h.A(h.Class("link"), h.Href("#"), h.Text("Register"), registerClick),
),
)
}

View File

@@ -12,27 +12,27 @@ func StatusBanner(g *game.Game, myColor int, playAgainClick h.H) h.H {
switch g.Status {
case game.StatusWaitingForPlayer:
message = "Waiting for opponent..."
class = "status waiting"
class = "alert bg-base-200 text-xl font-bold"
case game.StatusInProgress:
if g.CurrentTurn == myColor {
message = "Your turn!"
class = "status your-turn"
class = "alert alert-success text-xl font-bold"
} else {
opponentName := getOpponentName(g, myColor)
message = opponentName + "'s turn"
class = "status opponent-turn"
class = "alert bg-base-200 text-xl font-bold"
}
case game.StatusWon:
if g.Winner != nil && g.Winner.Color == myColor {
message = "You win!"
class = "status winner"
class = "alert alert-success text-xl font-bold"
} else if g.Winner != nil {
message = g.Winner.Nickname + " wins!"
class = "status loser"
class = "alert alert-error text-xl font-bold"
}
case game.StatusDraw:
message = "It's a draw!"
class = "status draw"
class = "alert alert-warning text-xl font-bold"
}
content := []h.H{
@@ -45,7 +45,7 @@ func StatusBanner(g *game.Game, myColor int, playAgainClick h.H) h.H {
if g.RematchGameID != nil {
content = append(content,
h.A(
h.Class("rematch-link"),
h.Class("btn btn-sm bg-white text-gray-800 border-none ml-4"),
h.Href("/game/"+*g.RematchGameID),
h.Text("Join Rematch"),
),
@@ -53,7 +53,7 @@ func StatusBanner(g *game.Game, myColor int, playAgainClick h.H) h.H {
} else if playAgainClick != nil {
content = append(content,
h.Button(
h.Class("play-again-btn"),
h.Class("btn btn-sm bg-white text-gray-800 border-none ml-4"),
h.Type("button"),
h.Text("Play again"),
playAgainClick,
@@ -103,12 +103,12 @@ func PlayerInfo(g *game.Game, myColor int) h.H {
opponentName = "Waiting..."
}
return h.Div(h.Class("player-info"),
h.Div(h.Class("player you"),
return h.Div(h.Class("flex gap-8 mb-2"),
h.Div(h.Class("flex items-center gap-2"),
h.Span(h.Class("player-chip "+myColorClass)),
h.Span(h.Text(myName+" (You)")),
),
h.Div(h.Class("player opponent"),
h.Div(h.Class("flex items-center gap-2"),
h.Span(h.Class("player-chip "+opponentColorClass)),
h.Span(h.Text(opponentName)),
),
@@ -119,13 +119,13 @@ const baseURL = "https://demo.adriatica.io"
func InviteLink(gameID string) h.H {
fullURL := baseURL + "/game/" + gameID
return h.Div(h.Class("invite-section"),
return h.Div(h.Class("mt-4 text-center"),
h.P(h.Text("Share this link with your opponent:")),
h.Div(h.Class("invite-link"),
h.Div(h.Class("bg-base-200 p-4 rounded-lg font-mono break-all my-2"),
h.Text(fullURL),
),
h.Button(
h.Class("copy-btn"),
h.Class("btn btn-sm mt-2"),
h.Type("button"),
h.Text("Copy Link"),
h.Attr("onclick", "navigator.clipboard.writeText('"+fullURL+"')"),