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:
93
assets/css/daisyui-theme.mjs
Normal file
93
assets/css/daisyui-theme.mjs
Normal file
File diff suppressed because one or more lines are too long
1026
assets/css/daisyui.mjs
Normal file
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
63
assets/css/input.css
Normal 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
1523
assets/css/output.css
Normal file
File diff suppressed because it is too large
Load Diff
3
go.mod
3
go.mod
@@ -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
2
go.sum
@@ -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
318
main.go
@@ -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);
|
||||
}
|
||||
`
|
||||
|
||||
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