From 7d46b94c4299879901096c591f797fcc4c39a4da Mon Sep 17 00:00:00 2001 From: AmanTahiliani Date: Fri, 27 Mar 2026 20:53:06 -0400 Subject: [PATCH] Updated Web UI --- internal/ui/dashboard.go | 48 ++-- internal/ui/standings.go | 16 +- internal/web/assets/app.js | 26 +++ internal/web/assets/index.html | 110 ++++++--- internal/web/assets/style.css | 411 +++++++++++++++++++++++++++++---- 5 files changed, 515 insertions(+), 96 deletions(-) diff --git a/internal/ui/dashboard.go b/internal/ui/dashboard.go index 6a49670..628a64e 100644 --- a/internal/ui/dashboard.go +++ b/internal/ui/dashboard.go @@ -167,46 +167,53 @@ func (m DashboardModel) View() string { titleStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color(colorF1Red)). - Bold(true) + Bold(true). + MarginBottom(1) now := time.Now() weekendUnderway := meetingHasStarted(*m.next, now) - sb.WriteString("\n") + // Left panel: Meeting Info & Countdown + var leftSide strings.Builder + leftSide.WriteString("\n") if weekendUnderway { - sb.WriteString(titleStyle.Render(fmt.Sprintf(" CURRENT RACE: %s", m.next.MeetingOfficialName)) + "\n") + leftSide.WriteString(titleStyle.Render(fmt.Sprintf("CURRENT RACE: %s", m.next.MeetingOfficialName)) + "\n") } else { - sb.WriteString(titleStyle.Render(fmt.Sprintf(" NEXT RACE: %s", m.next.MeetingOfficialName)) + "\n") + leftSide.WriteString(titleStyle.Render(fmt.Sprintf("NEXT RACE: %s", m.next.MeetingOfficialName)) + "\n") } - sb.WriteString(fmt.Sprintf(" %s • %s\n", countryFlag(m.next.CountryCode), m.next.Location)) + leftSide.WriteString(fmt.Sprintf(" %s • %s\n", countryFlag(m.next.CountryCode), m.next.Location)) currentSession, nextSession := currentAndNextSession(m.sessions, now) - sb.WriteString("\n") + leftSide.WriteString("\n") if currentSession != nil { - liveBadge := lipgloss.NewStyle().Background(lipgloss.Color(colorSoft)).Foreground(lipgloss.Color(colorSurface0)).Bold(true).Padding(0, 1).Render("LIVE") - sb.WriteString(fmt.Sprintf(" %s %s\n", liveBadge, currentSession.SessionName)) + liveBadge := lipgloss.NewStyle().Background(lipgloss.Color(colorF1Red)).Foreground(lipgloss.Color(colorWhite)).Bold(true).Padding(0, 1).Render("LIVE") + leftSide.WriteString(fmt.Sprintf(" %s %s\n", liveBadge, currentSession.SessionName)) } else if nextSession != nil { nextStart, err := sessionStartTime(*nextSession) if err != nil { - sb.WriteString(" " + lipgloss.NewStyle().Foreground(lipgloss.Color(colorMuted)).Render("Schedule unavailable") + "\n") + leftSide.WriteString(" " + lipgloss.NewStyle().Foreground(lipgloss.Color(colorMuted)).Render("Schedule unavailable") + "\n") } else { diff := nextStart.Sub(now) days := int(diff.Hours() / 24) hours := int(diff.Hours()) % 24 mins := int(diff.Minutes()) % 60 secs := int(diff.Seconds()) % 60 - sb.WriteString(fmt.Sprintf(" Next: %s in %dd %02dh %02dm %02ds\n", nextSession.SessionName, days, hours, mins, secs)) + + countdown := fmt.Sprintf("%dd %02dh %02dm %02ds", days, hours, mins, secs) + leftSide.WriteString(fmt.Sprintf(" Next: %s\n", styleBold.Render(nextSession.SessionName))) + leftSide.WriteString(fmt.Sprintf(" Starts in: %s\n", styleCountdown.Render(countdown))) } } else { - sb.WriteString(" " + lipgloss.NewStyle().Foreground(lipgloss.Color(colorMuted)).Render("Weekend Finished") + "\n") + leftSide.WriteString(" " + lipgloss.NewStyle().Foreground(lipgloss.Color(colorMuted)).Render("Weekend Finished") + "\n") } - sb.WriteString("\n") + leftPanel := stylePanelBorder.Width(w/2 - 4).Render(leftSide.String()) - // Weekend Schedule + // Right panel: Weekend Schedule + var rightSide strings.Builder if len(m.sessions) > 0 { - sb.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color(colorWhite)).Bold(true).Render(" WEEKEND SCHEDULE (Local Time)") + "\n") + rightSide.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color(colorWhite)).Bold(true).Render("WEEKEND SCHEDULE (Local Time)") + "\n\n") for _, s := range m.sessions { stLocal, err := sessionStartTime(s) if err != nil { @@ -219,13 +226,20 @@ func (m DashboardModel) View() string { if end, err := sessionEndTime(s); err == nil && !now.Before(end) { rowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(colorMuted)) } else if currentSession != nil && currentSession.SessionKey == s.SessionKey { - rowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(colorSoft)).Bold(true) + rowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(colorF1Red)).Bold(true) } - sb.WriteString(rowStyle.Render(fmt.Sprintf(" %-15s %-12s %s", s.SessionName, day, tStr)) + "\n") + rightSide.WriteString(rowStyle.Render(fmt.Sprintf("%-15s %-12s %s", s.SessionName, day, tStr)) + "\n") } } + rightPanel := stylePanelBorder.Width(w/2 - 4).Render(rightSide.String()) - sb.WriteString("\n" + helpBar("1-6 tabs", "q quit")) + if m.width > 80 { + sb.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, leftPanel, " ", rightPanel)) + } else { + sb.WriteString(leftPanel + "\n" + rightPanel) + } + + sb.WriteString("\n\n" + helpBar("1-7 tabs", "q quit")) return sb.String() } diff --git a/internal/ui/standings.go b/internal/ui/standings.go index 80ebfc0..39bd625 100644 --- a/internal/ui/standings.go +++ b/internal/ui/standings.go @@ -347,13 +347,23 @@ func (m StandingsModel) renderDriverStandings() string { colorBar := lipgloss.NewStyle().Foreground(lipgloss.Color(teamColor)).Render("┃") pointsBar := renderPointsBar(s.PointsCurrent, maxPoints, barWidth, teamColor) + // Styling for top 3 + nameStyle := lipgloss.NewStyle() + if i == 0 { + nameStyle = stylePositionFirst + } else if i == 1 { + nameStyle = stylePositionSecond + } else if i == 2 { + nameStyle = stylePositionThird + } + var row string if compact { row = fmt.Sprintf(" %s %s %s %s %s %s", padRightVisible(pos, 3), delta, colorBar, - padRight(acronym, 4), + nameStyle.Render(padRight(acronym, 4)), padRight(truncate(team, teamWidth), teamWidth), padLeft(fmt.Sprintf("%.0f", s.PointsCurrent), 5), ) @@ -362,8 +372,8 @@ func (m StandingsModel) renderDriverStandings() string { padRightVisible(pos, 4), delta, colorBar, - padRight(acronym, 4), - padRight(truncate(name, nameWidth), nameWidth), + nameStyle.Render(padRight(acronym, 4)), + nameStyle.Render(padRight(truncate(name, nameWidth), nameWidth)), padRight(truncate(team, teamWidth), teamWidth), padLeft(fmt.Sprintf("%.0f", s.PointsCurrent), 6), pointsBar, diff --git a/internal/web/assets/app.js b/internal/web/assets/app.js index 6e2da5a..601d88a 100644 --- a/internal/web/assets/app.js +++ b/internal/web/assets/app.js @@ -615,6 +615,32 @@ function standingsPage() { : []; } catch (_) { this.teamStandings = []; } }, + + standingsBarWidth(pts, list) { + if (!list || !list.length) return 0; + const max = list[0].points_current; + return max > 0 ? Math.round((pts / max) * 100) : 0; + }, + + teamColor(name) { + // Map known constructor names to brand colors + const map = { + 'Red Bull Racing': '#3671C6', + 'Mercedes': '#27F4D2', + 'Ferrari': '#E8002D', + 'McLaren': '#FF8000', + 'Aston Martin': '#229971', + 'Alpine': '#FF87BC', + 'Williams': '#64C4FF', + 'Racing Bulls': '#6692FF', + 'Kick Sauber': '#52E252', + 'Haas F1 Team': '#B6BABD', + }; + for (const key of Object.keys(map)) { + if (name && name.includes(key.split(' ')[0])) return map[key]; + } + return '#666688'; + }, }; } diff --git a/internal/web/assets/index.html b/internal/web/assets/index.html index b55a8e5..0706f50 100644 --- a/internal/web/assets/index.html +++ b/internal/web/assets/index.html @@ -40,6 +40,16 @@
+ + + +
+ +

@@ -51,8 +61,10 @@
DRIVERS' CHAMPIONSHIP
-