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

File diff suppressed because one or more lines are too long

1026
assets/css/daisyui.mjs Normal file

File diff suppressed because one or more lines are too long

63
assets/css/input.css Normal file
View File

@@ -0,0 +1,63 @@
@import 'tailwindcss';
@source not "./daisyui{,*}.mjs";
@plugin "./daisyui.mjs";
@plugin "./daisyui-theme.mjs" {
name: "connect4";
default: true;
color-scheme: light;
/* Playful & bright — warm, casual board game feel */
--color-base-100: oklch(98% 0.005 90); /* warm white */
--color-base-200: oklch(95% 0.01 90); /* light cream */
--color-base-300: oklch(90% 0.015 85); /* warm gray border */
--color-base-content: oklch(25% 0.02 50); /* dark warm text */
--color-primary: oklch(60% 0.22 30); /* warm red — matches game pieces */
--color-primary-content: oklch(98% 0.005 90);
--color-secondary: oklch(82% 0.17 85); /* golden yellow — matches game pieces */
--color-secondary-content: oklch(25% 0.02 50);
--color-accent: oklch(65% 0.19 250); /* board blue */
--color-accent-content: oklch(98% 0.005 90);
--color-neutral: oklch(35% 0.03 260);
--color-neutral-content: oklch(95% 0.01 90);
--color-success: oklch(72% 0.19 155);
--color-success-content: oklch(20% 0.04 155);
--color-warning: oklch(82% 0.17 85);
--color-warning-content: oklch(25% 0.04 85);
--color-error: oklch(60% 0.22 30);
--color-error-content: oklch(97% 0.01 30);
--color-info: oklch(70% 0.15 240);
--color-info-content: oklch(20% 0.04 240);
--radius-selector: 0.5rem;
--radius-field: 0.5rem;
--radius-box: 0.75rem;
--border: 1px;
--depth: 1;
--noise: 0;
};
/* Game-specific styles that can't be Tailwind utilities */
.board {
display: flex;
gap: 8px;
background: #2563eb;
padding: 16px;
border-radius: 12px;
}
.column { display: flex; flex-direction: column; gap: 8px; padding: 4px; border-radius: 8px; }
.column.clickable { cursor: pointer; }
.column.clickable:hover { background: rgba(255,255,255,0.15); }
.cell { width: 48px; height: 48px; border-radius: 50%; background: #1e40af; transition: background 0.2s; }
.cell.red { background: #dc2626; }
.cell.yellow { background: #facc15; }
.cell.winning { animation: pulse 0.5s ease-in-out infinite alternate; }
@keyframes pulse {
from { transform: scale(1); box-shadow: 0 0 10px rgba(255,255,255,0.5); }
to { transform: scale(1.1); box-shadow: 0 0 20px rgba(255,255,255,0.8); }
}
.player-chip { width: 20px; height: 20px; border-radius: 50%; background: #666; }
.player-chip.red { background: #dc2626; }
.player-chip.yellow { background: #facc15; }

1523
assets/css/output.css Normal file

File diff suppressed because it is too large Load Diff

3
go.mod
View File

@@ -16,6 +16,7 @@ require (
github.com/alexedwards/scs/v2 v2.9.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/hookenz/gotailwind/v4 v4.1.18 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
@@ -33,3 +34,5 @@ require (
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
tool github.com/hookenz/gotailwind/v4

2
go.sum
View File

@@ -20,6 +20,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hookenz/gotailwind/v4 v4.1.18 h1:1h3XwTVx1dEBm6A0bcosAplNCde+DCmVJG0arLy5fBE=
github.com/hookenz/gotailwind/v4 v4.1.18/go.mod h1:IfiJtdp8ExV9HV2XUiVjRBvB3QewVXVKWoAGEcpjfNE=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=

318
main.go
View File

@@ -3,7 +3,9 @@ package main
import (
"context"
"database/sql"
_ "embed"
"log"
"net/http"
"github.com/google/uuid"
"github.com/ryanhamamura/c4/auth"
@@ -18,6 +20,17 @@ import (
var store = game.NewGameStore()
var queries *gen.Queries
//go:embed assets/css/output.css
var daisyUICSS []byte
func DaisyUIPlugin(v *via.V) {
v.HTTPServeMux().HandleFunc("GET /_plugins/daisyui/style.css", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css")
_, _ = w.Write(daisyUICSS)
})
v.AppendToHead(h.Link(h.Rel("stylesheet"), h.Href("/_plugins/daisyui/style.css")))
}
func main() {
if err := db.Init("c4.db"); err != nil {
log.Fatal(err)
@@ -36,13 +49,9 @@ func main() {
DocumentTitle: "Connect 4",
ServerAddress: ":7331",
SessionManager: sessionManager,
Plugins: []via.Plugin{DaisyUIPlugin},
})
v.AppendToHead(
h.Link(h.Rel("stylesheet"), h.Href("https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css")),
h.StyleEl(h.Raw(gameCSS)),
)
// Home page - enter nickname and create game
v.Page("/", func(c *via.Context) {
userID := c.Session().GetString("user_id")
@@ -380,7 +389,7 @@ func main() {
var content []h.H
content = append(content,
h.H1(h.Text("Connect 4")),
h.H1(h.Class("text-3xl font-bold"), h.Text("Connect 4")),
ui.PlayerInfo(g, myColor),
ui.StatusBanner(g, myColor, createRematch.OnClick()),
ui.BoardComponent(g, columnClick, myColor),
@@ -391,7 +400,7 @@ func main() {
content = append(content, ui.InviteLink(g.ID))
}
mainAttrs := []h.H{h.Class("container game-container")}
mainAttrs := []h.H{h.Class("flex flex-col items-center gap-4 p-4")}
mainAttrs = append(mainAttrs, content...)
return h.Main(mainAttrs...)
})
@@ -399,298 +408,3 @@ func main() {
v.Start()
}
const gameCSS = `
body { margin: 0; }
.lobby {
max-width: 400px;
margin: 2rem auto;
text-align: center;
}
.game-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 1rem;
}
.board {
display: flex;
gap: 8px;
background: #2563eb;
padding: 16px;
border-radius: 12px;
}
.column {
display: flex;
flex-direction: column;
gap: 8px;
padding: 4px;
border-radius: 8px;
}
.column.clickable {
cursor: pointer;
}
.column.clickable:hover {
background: rgba(255,255,255,0.15);
}
.cell {
width: 48px;
height: 48px;
border-radius: 50%;
background: #1e40af;
transition: background 0.2s;
}
.cell.red {
background: #dc2626;
}
.cell.yellow {
background: #facc15;
}
.cell.winning {
animation: pulse 0.5s ease-in-out infinite alternate;
}
@keyframes pulse {
from { transform: scale(1); box-shadow: 0 0 10px rgba(255,255,255,0.5); }
to { transform: scale(1.1); box-shadow: 0 0 20px rgba(255,255,255,0.8); }
}
.status {
font-size: 1.25rem;
font-weight: bold;
padding: 0.5rem 1rem;
border-radius: 8px;
}
.status.waiting {
background: var(--pico-muted-background);
}
.status.your-turn {
background: #22c55e;
color: white;
}
.status.opponent-turn {
background: var(--pico-muted-background);
}
.status.winner {
background: #22c55e;
color: white;
}
.status.loser {
background: #dc2626;
color: white;
}
.status.draw {
background: #f59e0b;
color: white;
}
.play-again-btn, .rematch-link {
margin-left: 1rem;
padding: 0.25rem 0.75rem;
font-size: 0.875rem;
background: white;
color: #333;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
}
.play-again-btn:hover, .rematch-link:hover {
background: #eee;
}
.player-info {
display: flex;
gap: 2rem;
margin-bottom: 0.5rem;
}
.player {
display: flex;
align-items: center;
gap: 0.5rem;
}
.player-chip {
width: 20px;
height: 20px;
border-radius: 50%;
background: #666;
}
.player-chip.red {
background: #dc2626;
}
.player-chip.yellow {
background: #facc15;
}
.invite-section {
margin-top: 1rem;
text-align: center;
}
.invite-link {
background: var(--pico-muted-background);
padding: 1rem;
border-radius: 8px;
font-family: monospace;
word-break: break-all;
margin: 0.5rem 0;
}
.copy-btn {
margin-top: 0.5rem;
}
.auth-header {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
padding: 0.5rem;
background: var(--pico-muted-background);
border-radius: 8px;
}
.auth-header button {
margin: 0;
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.guest-banner {
margin-bottom: 1rem;
padding: 0.5rem;
background: var(--pico-muted-background);
border-radius: 8px;
font-size: 0.875rem;
}
.error {
color: #dc2626;
background: #fef2f2;
padding: 0.5rem 1rem;
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;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: var(--pico-muted-background);
border-radius: 8px;
transition: background 0.2s;
}
.game-entry:hover {
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;
}
.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);
}
.join-options {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin: 1rem 0;
}
.register-link {
font-size: 0.875rem;
color: var(--pico-muted-color);
}
`

View File

@@ -7,18 +7,18 @@ 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")),
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.Label(h.Text("Username"), h.Attr("for", "username")),
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"),
@@ -26,8 +26,9 @@ func LoginView(usernameBind, passwordBind, loginKeyDown, loginClick h.H, errorMs
h.Attr("required"),
h.Attr("autofocus"),
),
h.Label(h.Text("Password"), h.Attr("for", "password")),
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"),
@@ -37,6 +38,7 @@ func LoginView(usernameBind, passwordBind, loginKeyDown, loginClick h.H, errorMs
),
),
h.Button(
h.Class("btn btn-primary w-full"),
h.Type("button"),
h.Text("Login"),
loginClick,
@@ -44,8 +46,7 @@ func LoginView(usernameBind, passwordBind, loginKeyDown, loginClick h.H, errorMs
),
h.P(
h.Text("Don't have an account? "),
h.A(h.Href("/register"), h.Text("Register")),
),
h.A(h.Class("link"), h.Href("/register"), h.Text("Register")),
),
)
}
@@ -53,18 +54,18 @@ func LoginView(usernameBind, passwordBind, loginKeyDown, loginClick h.H, errorMs
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")),
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.Label(h.Text("Username"), h.Attr("for", "username")),
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"),
@@ -72,16 +73,18 @@ func RegisterView(usernameBind, passwordBind, confirmBind, registerKeyDown, regi
h.Attr("required"),
h.Attr("autofocus"),
),
h.Label(h.Text("Password"), h.Attr("for", "password")),
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.Text("Confirm Password"), h.Attr("for", "confirm")),
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"),
@@ -91,6 +94,7 @@ func RegisterView(usernameBind, passwordBind, confirmBind, registerKeyDown, regi
),
),
h.Button(
h.Class("btn btn-primary w-full"),
h.Type("button"),
h.Text("Register"),
registerClick,
@@ -98,18 +102,17 @@ func RegisterView(usernameBind, passwordBind, confirmBind, registerKeyDown, regi
),
h.P(
h.Text("Already have an account? "),
h.A(h.Href("/login"), h.Text("Login")),
),
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,15 +12,15 @@ func LobbyView(nicknameBind, createGameKeyDown, createGameClick h.H, isLoggedIn
authSection = GuestBanner()
}
return h.Main(h.Class("container"),
h.Div(h.Class("lobby"),
return h.Main(h.Class("max-w-sm mx-auto mt-8 text-center"),
authSection,
h.H1(h.Text("Connect 4")),
h.P(h.Text("Challenge a friend to a game of Connect 4!")),
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.Label(h.Text("Your Nickname"), h.Attr("for", "nickname")),
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"),
@@ -30,25 +30,25 @@ func LobbyView(nicknameBind, createGameKeyDown, createGameClick h.H, isLoggedIn
),
),
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.")),
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.Label(h.Text("Your Nickname"), h.Attr("for", "nickname")),
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"),
@@ -59,37 +59,36 @@ func NicknamePrompt(nicknameBind, setNicknameKeyDown, setNicknameClick h.H) h.H
),
),
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"),
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.Button(
h.Class("btn btn-secondary w-full"),
h.Type("button"),
h.Class("secondary"),
h.Text("Continue as Guest"),
guestClick,
),
),
h.P(h.Class("register-link"),
h.P(h.Class("text-sm opacity-60"),
h.Text("Don't have an account? "),
h.A(h.Href("#"), h.Text("Register"), registerClick),
),
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+"')"),