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:
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);
|
||||
}
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user