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.
111 lines
2.5 KiB
Go
111 lines
2.5 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/ryanhamamura/c4/game"
|
|
"github.com/ryanhamamura/via/h"
|
|
)
|
|
|
|
type GameListItem struct {
|
|
ID string
|
|
Status int
|
|
OpponentName string
|
|
IsMyTurn bool
|
|
LastPlayed time.Time
|
|
}
|
|
|
|
func GameList(games []GameListItem, deleteClick func(id string) h.H) h.H {
|
|
if len(games) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var items []h.H
|
|
for _, g := range games {
|
|
items = append(items, gameListEntry(g, deleteClick))
|
|
}
|
|
|
|
listItems := []h.H{h.Class("flex flex-col gap-2")}
|
|
listItems = append(listItems, items...)
|
|
|
|
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...),
|
|
)
|
|
}
|
|
|
|
func gameListEntry(g GameListItem, deleteClick func(id string) h.H) h.H {
|
|
statusText, statusClass := getStatusDisplay(g)
|
|
|
|
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("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.Span(h.Class("text-xs opacity-60"), h.Text(formatTimeAgo(g.LastPlayed))),
|
|
),
|
|
),
|
|
h.Button(
|
|
h.Type("button"),
|
|
h.Class("btn btn-ghost btn-sm btn-square hover:btn-error"),
|
|
h.Text("\u00d7"),
|
|
deleteClick(g.ID),
|
|
),
|
|
)
|
|
}
|
|
|
|
func getStatusDisplay(g GameListItem) (string, string) {
|
|
switch game.GameStatus(g.Status) {
|
|
case game.StatusWaitingForPlayer:
|
|
return "Waiting for opponent", "text-sm opacity-60"
|
|
case game.StatusInProgress:
|
|
if g.IsMyTurn {
|
|
return "Your turn!", "text-sm text-success font-bold"
|
|
}
|
|
return "Opponent's turn", "text-sm"
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
func getOpponentDisplay(g GameListItem) string {
|
|
if g.OpponentName == "" {
|
|
return "Waiting for opponent..."
|
|
}
|
|
return "vs " + g.OpponentName
|
|
}
|
|
|
|
func formatTimeAgo(t time.Time) string {
|
|
if t.IsZero() {
|
|
return ""
|
|
}
|
|
duration := time.Since(t)
|
|
|
|
if duration < time.Minute {
|
|
return "just now"
|
|
}
|
|
if duration < time.Hour {
|
|
mins := int(duration.Minutes())
|
|
if mins == 1 {
|
|
return "1 minute ago"
|
|
}
|
|
return fmt.Sprintf("%d minutes ago", mins)
|
|
}
|
|
if duration < 24*time.Hour {
|
|
hours := int(duration.Hours())
|
|
if hours == 1 {
|
|
return "1 hour ago"
|
|
}
|
|
return fmt.Sprintf("%d hours ago", hours)
|
|
}
|
|
days := int(duration.Hours() / 24)
|
|
if days == 1 {
|
|
return "yesterday"
|
|
}
|
|
return fmt.Sprintf("%d days ago", days)
|
|
}
|