package ui import ( "fmt" "strings" "github.com/charmbracelet/lipgloss" "github.com/humdrum-tiv/sportsball/internal/model" ) const cardWidth = 34 // renderCard draws one game as a bordered card. pulse in [0,1] drives the // live indicator brightness; selected swaps in the accent border. favAway/ // favHome mark a team with a star. func renderCard(g model.Game, selected bool, pulse float64, favAway, favHome bool) string { var b strings.Builder b.WriteString(statusLine(g, pulse, selected)) b.WriteString("\n") b.WriteString(teamRow(g.Away, g, g.Away.Winner, favAway)) b.WriteString("\n") b.WriteString(teamRow(g.Home, g, g.Home.Winner, favHome)) style := styleCard if selected { style = styleCardSelected } return style.Width(cardWidth).Render(b.String()) } // statusLine is the card header: state badge + clock/detail, right-aligned. // When selected, a leading accent marker reinforces the highlight border. func statusLine(g model.Game, pulse float64, selected bool) string { var left string switch g.State { case model.StateLive: left = liveDot(pulse) + " " + lipgloss.NewStyle(). Foreground(colLive).Bold(true).Render("LIVE") case model.StateFinal: left = styleFinal.Render("◼ FINAL") default: left = styleSubtle.Render("○ " + startLabel(g)) } marker := " " if selected { marker = lipgloss.NewStyle().Foreground(colAccent).Bold(true).Render("▸ ") } left = marker + left // Pre-game already shows the start time on the left; ESPN's verbose detail // ("Tue, June 16th at 3:00 PM EDT") would just duplicate it. Show clock // while live, status detail when final, nothing pre-game. detail := "" switch g.State { case model.StateLive: if g.Clock != "" && g.Clock != "0:00" { detail = g.Clock } else { detail = g.Detail } case model.StateFinal: detail = g.Detail } right := styleFaint.Render(truncate(detail, 14)) gap := cardWidth - 2 - lipgloss.Width(left) - lipgloss.Width(right) if gap < 1 { gap = 1 } return left + strings.Repeat(" ", gap) + right } // teamRow renders one team: abbr (in team color) + name, score on the right. // fav prefixes a star. func teamRow(t model.Team, g model.Game, leading, fav bool) string { abbr := lipgloss.NewStyle(). Foreground(teamColor(t.Color)). Bold(true). Render(fmt.Sprintf("%-3s", t.Abbr)) star := "" if fav { star = lipgloss.NewStyle().Foreground(colWarn).Render("★") } else { star = " " } name := star + styleSubtle.Render(truncate(t.Name, 13)) left := abbr + " " + name score := " -" if g.Started() { ss := styleScore if leading && g.Started() { ss = styleWin } score = ss.Render(fmt.Sprintf("%3d", t.Score)) } gap := cardWidth - 2 - lipgloss.Width(left) - lipgloss.Width(score) if gap < 1 { gap = 1 } return left + strings.Repeat(" ", gap) + score } // startLabel is the short local start time for pre-game cards. func startLabel(g model.Game) string { if g.Start.IsZero() { return "TBD" } return g.Start.Format("Mon 3:04 PM") } func truncate(s string, n int) string { if lipgloss.Width(s) <= n { return s } if n <= 1 { return s[:n] } return s[:n-1] + "…" }