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:
161
ui/auth.go
161
ui/auth.go
@@ -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."),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 "", ""
|
||||
}
|
||||
|
||||
123
ui/lobby.go
123
ui/lobby.go
@@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
28
ui/status.go
28
ui/status.go
@@ -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+"')"),
|
||||
|
||||
Reference in New Issue
Block a user