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 }