▍ humdrum codex / sportsball v0.1.0
license AGPL-3.0
3.9 KB raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package ui

import (
	"fmt"
	"strings"

	"github.com/charmbracelet/lipgloss"

	"github.com/humdrum-tiv/sportsball/internal/model"
)

// scheduleView renders one team's full season schedule: a scrollable list of
// past results and upcoming fixtures, with an "upcoming" divider between them.
func (a App) scheduleView() string {
	t := a.schedule.Team
	l, _ := model.LeagueByID(a.scheduleLeague)
	name := t.FullName
	if name == "" {
		name = t.Name
	}
	title := styleTitle.Render(" SCHEDULE ") + "  " +
		lipgloss.NewStyle().Foreground(teamColor(t.Color)).Bold(true).Render(l.Icon+" "+name)
	if a.schedule.Season != "" {
		title += "  " + styleSubtle.Render(a.schedule.Season)
	}
	footer := styleHelp.Render(strings.Join([]string{
		keys.Up.Help().Key + "/" + keys.Down.Help().Key + " scroll",
		keys.Refresh.Help().Key + " refresh",
		keys.Back.Help().Key + " back",
	}, styleFaint.Render("  ·  ")))

	frame := func(body string) string {
		return styleApp.Render(strings.Join([]string{title, "", body, "", footer}, "\n"))
	}

	switch {
	case a.scheduleErr != nil:
		return frame(styleSubtle.Render("schedule unavailable — " + a.scheduleErr.Error()))
	case len(a.schedule.Games) == 0:
		return frame(styleSubtle.Render(a.spinner.View() + " loading schedule…"))
	}

	lines, _ := a.scheduleLines()
	avail := a.scheduleAvail()
	off := a.scheduleScroll
	if off > len(lines)-avail {
		off = len(lines) - avail
	}
	if off < 0 {
		off = 0
	}
	end := off + avail
	if end > len(lines) {
		end = len(lines)
	}
	return frame(strings.Join(lines[off:end], "\n"))
}

func (a App) scheduleAvail() int {
	avail := a.height - 5 // title, blank, blank, footer (+1 slack)
	if avail < 3 {
		avail = 3
	}
	return avail
}

func (a App) scheduleScrollMax() int {
	lines, _ := a.scheduleLines()
	if m := len(lines) - a.scheduleAvail(); m > 0 {
		return m
	}
	return 0
}

// scheduleLines renders each game to a line and reports the line index of the
// first upcoming game, so the view can open at "now" rather than the top.
func (a App) scheduleLines() (lines []string, upcomingTop int) {
	t := a.schedule.Team
	dividerDone := false
	for _, g := range a.schedule.Games {
		if !dividerDone && g.State == model.StatePre {
			upcomingTop = len(lines)
			lines = append(lines, styleFaint.Render("  ── upcoming ──"))
			dividerDone = true
		}
		lines = append(lines, a.scheduleRow(g, t))
	}
	return lines, upcomingTop
}

// scheduleRow formats one game from the perspective of team t: date, home/away
// marker, opponent, and the result (W/L + score for finals, time for upcoming).
func (a App) scheduleRow(g model.Game, t model.Team) string {
	home := g.Home.ID == t.ID
	opp := g.Home
	if home {
		opp = g.Away
	}

	date := styleSubtle.Render(fmt.Sprintf("%-10s", g.Start.Format("Mon Jan 2")))
	loc := "vs"
	if !home {
		loc = "@ "
	}
	oppAbbr := lipgloss.NewStyle().Foreground(teamColor(opp.Color)).Bold(true).
		Render(fmt.Sprintf("%-3s", opp.Abbr))
	oppName := truncate(opp.Name, 16)
	oppName += strings.Repeat(" ", max(0, 16-lipgloss.Width(oppName)))

	var result string
	switch g.State {
	case model.StateLive:
		us, them := teamScores(g, home)
		result = lipgloss.NewStyle().Foreground(colLive).Bold(true).
			Render(fmt.Sprintf("LIVE %d-%d", us, them))
	case model.StateFinal:
		us, them := teamScores(g, home)
		won := (home && g.Home.Winner) || (!home && g.Away.Winner)
		mark, style := "L", styleFinal
		switch {
		case won:
			mark, style = "W", styleWin
		case us == them:
			mark = "T"
		}
		result = style.Render(fmt.Sprintf("%s %d-%d", mark, us, them))
	default:
		result = styleSubtle.Render(startLabel(g))
	}

	return fmt.Sprintf("%s  %s %s %s  %s", date, styleSubtle.Render(loc), oppAbbr, oppName, result)
}

// teamScores returns (team's score, opponent's score) given whether the team is
// home in g.
func teamScores(g model.Game, home bool) (us, them int) {
	if home {
		return g.Home.Score, g.Away.Score
	}
	return g.Away.Score, g.Home.Score
}