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

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);
}
`