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,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+"')"),