mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-08 04:06:18 -04:00
Updated UI default next session detail
This commit is contained in:
@@ -55,12 +55,22 @@ type AppModel struct {
|
|||||||
trackMap TrackMapModel
|
trackMap TrackMapModel
|
||||||
|
|
||||||
meetings []models.Meeting
|
meetings []models.Meeting
|
||||||
|
nextInfo *statusBarTarget
|
||||||
|
|
||||||
// Splash screen state
|
// Splash screen state
|
||||||
showSplash bool
|
showSplash bool
|
||||||
splashSpinner spinner.Model
|
splashSpinner spinner.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type statusBarTarget struct {
|
||||||
|
meeting *models.Meeting
|
||||||
|
current *models.Session
|
||||||
|
next *models.Session
|
||||||
|
loading bool
|
||||||
|
stale bool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
func NewAppModel(client *api.OpenF1Client) AppModel {
|
func NewAppModel(client *api.OpenF1Client) AppModel {
|
||||||
year := time.Now().Year()
|
year := time.Now().Year()
|
||||||
|
|
||||||
@@ -105,6 +115,43 @@ func prefetchTrackOutlines(client *api.OpenF1Client, year int) tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type statusBarScheduleLoadedMsg struct {
|
||||||
|
target *statusBarTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchStatusBarSchedule(client *api.OpenF1Client, meetings []models.Meeting) tea.Cmd {
|
||||||
|
meetingsCopy := append([]models.Meeting(nil), meetings...)
|
||||||
|
return func() tea.Msg {
|
||||||
|
now := time.Now()
|
||||||
|
target := &statusBarTarget{}
|
||||||
|
|
||||||
|
meeting := currentMeeting(meetingsCopy, now)
|
||||||
|
if meeting == nil {
|
||||||
|
meeting = nextUpcomingMeeting(meetingsCopy, now)
|
||||||
|
}
|
||||||
|
if meeting == nil {
|
||||||
|
return statusBarScheduleLoadedMsg{target: target}
|
||||||
|
}
|
||||||
|
|
||||||
|
target.meeting = meeting
|
||||||
|
target.loading = true
|
||||||
|
|
||||||
|
sessions, err := client.GetSessionsForMeeting(int(meeting.MeetingKey))
|
||||||
|
if err != nil {
|
||||||
|
target.loading = false
|
||||||
|
target.err = err
|
||||||
|
target.stale = client.LastResponseWasStale()
|
||||||
|
return statusBarScheduleLoadedMsg{target: target}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortSessionsByStart(sessions)
|
||||||
|
target.current, target.next = currentAndNextSession(sessions, now)
|
||||||
|
target.loading = false
|
||||||
|
target.stale = client.LastResponseWasStale()
|
||||||
|
return statusBarScheduleLoadedMsg{target: target}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m AppModel) Init() tea.Cmd {
|
func (m AppModel) Init() tea.Cmd {
|
||||||
return tea.Batch(
|
return tea.Batch(
|
||||||
m.dashboard.Init(),
|
m.dashboard.Init(),
|
||||||
@@ -268,12 +315,18 @@ func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case meetingsLoadedMsg:
|
case meetingsLoadedMsg:
|
||||||
if msg.err == nil {
|
if msg.err == nil {
|
||||||
m.meetings = msg.meetings
|
m.meetings = msg.meetings
|
||||||
|
m.nextInfo = &statusBarTarget{loading: true}
|
||||||
|
cmds = append(cmds, fetchStatusBarSchedule(m.client, msg.meetings))
|
||||||
}
|
}
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
m.calendar, cmd = m.calendar.Update(msg)
|
m.calendar, cmd = m.calendar.Update(msg)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
return m, tea.Batch(cmds...)
|
return m, tea.Batch(cmds...)
|
||||||
|
|
||||||
|
case statusBarScheduleLoadedMsg:
|
||||||
|
m.nextInfo = msg.target
|
||||||
|
return m, nil
|
||||||
|
|
||||||
case sessionResultsLoadedMsg:
|
case sessionResultsLoadedMsg:
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
m.raceDetail, cmd = m.raceDetail.Update(msg)
|
m.raceDetail, cmd = m.raceDetail.Update(msg)
|
||||||
@@ -521,39 +574,53 @@ func (m AppModel) renderStatusBar(width int) string {
|
|||||||
Render("BOX-BOX"),
|
Render("BOX-BOX"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next race countdown
|
if m.nextInfo != nil && m.nextInfo.meeting != nil {
|
||||||
var nextMeeting *models.Meeting
|
flag := countryFlag(m.nextInfo.meeting.CountryCode)
|
||||||
for i := range m.meetings {
|
raceName := styleStatusValue.Render(m.nextInfo.meeting.MeetingName)
|
||||||
start, err := time.Parse(time.RFC3339, m.meetings[i].DateStart)
|
|
||||||
if err != nil {
|
|
||||||
start, _ = time.Parse("2006-01-02", m.meetings[i].DateStart[:min(len(m.meetings[i].DateStart), 10)])
|
|
||||||
}
|
|
||||||
if start.After(now) {
|
|
||||||
nextMeeting = &m.meetings[i]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if nextMeeting != nil {
|
leftParts = append(leftParts, styleStatusLabel.Render("│"))
|
||||||
start, err := time.Parse(time.RFC3339, nextMeeting.DateStart)
|
|
||||||
if err == nil {
|
switch {
|
||||||
|
case m.nextInfo.current != nil:
|
||||||
|
leftParts = append(leftParts,
|
||||||
|
styleStatusLabel.Render("LIVE"),
|
||||||
|
flag+" "+raceName,
|
||||||
|
styleStatusLabel.Render("·"),
|
||||||
|
styleStatusValue.Render(m.nextInfo.current.SessionName),
|
||||||
|
)
|
||||||
|
case m.nextInfo.next != nil:
|
||||||
|
if start, err := sessionStartTime(*m.nextInfo.next); err == nil {
|
||||||
|
diff := start.Sub(now)
|
||||||
|
days := int(diff.Hours() / 24)
|
||||||
|
hours := int(diff.Hours()) % 24
|
||||||
|
leftParts = append(leftParts,
|
||||||
|
styleStatusLabel.Render("NEXT"),
|
||||||
|
flag+" "+raceName,
|
||||||
|
styleStatusLabel.Render("·"),
|
||||||
|
styleStatusValue.Render(m.nextInfo.next.SessionName),
|
||||||
|
styleStatusLabel.Render("in"),
|
||||||
|
styleCountdown.Render(fmt.Sprintf("%dd %dh", days, hours)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case meetingHasStarted(*m.nextInfo.meeting, now):
|
||||||
|
leftParts = append(leftParts,
|
||||||
|
styleStatusLabel.Render("DONE"),
|
||||||
|
flag+" "+raceName,
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
if start, err := meetingStartTime(*m.nextInfo.meeting); err == nil {
|
||||||
diff := start.Sub(now)
|
diff := start.Sub(now)
|
||||||
days := int(diff.Hours() / 24)
|
days := int(diff.Hours() / 24)
|
||||||
hours := int(diff.Hours()) % 24
|
hours := int(diff.Hours()) % 24
|
||||||
|
|
||||||
flag := countryFlag(nextMeeting.CountryCode)
|
|
||||||
raceName := styleStatusValue.Render(nextMeeting.MeetingName)
|
|
||||||
countdown := styleCountdown.Render(fmt.Sprintf("%dd %dh", days, hours))
|
|
||||||
|
|
||||||
leftParts = append(leftParts,
|
leftParts = append(leftParts,
|
||||||
styleStatusLabel.Render("│"),
|
|
||||||
styleStatusLabel.Render("NEXT"),
|
styleStatusLabel.Render("NEXT"),
|
||||||
flag+" "+raceName,
|
flag+" "+raceName,
|
||||||
styleStatusLabel.Render("in"),
|
styleStatusLabel.Render("in"),
|
||||||
countdown,
|
styleCountdown.Render(fmt.Sprintf("%dd %dh", days, hours)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
left := strings.Join(leftParts, " ")
|
left := strings.Join(leftParts, " ")
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -64,10 +63,7 @@ func fetchDashboardSessions(client *api.OpenF1Client, meetingKey int) tea.Cmd {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return dashboardSessionsLoadedMsg{err: err}
|
return dashboardSessionsLoadedMsg{err: err}
|
||||||
}
|
}
|
||||||
// Sort by DateStart
|
sortSessionsByStart(sessions)
|
||||||
sort.Slice(sessions, func(i, j int) bool {
|
|
||||||
return sessions[i].DateStart < sessions[j].DateStart
|
|
||||||
})
|
|
||||||
return dashboardSessionsLoadedMsg{sessions: sessions}
|
return dashboardSessionsLoadedMsg{sessions: sessions}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,29 +109,9 @@ func (m DashboardModel) Update(msg tea.Msg) (DashboardModel, tea.Cmd) {
|
|||||||
m.stale = true
|
m.stale = true
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
m.next = currentMeeting(m.meetings, now)
|
||||||
// First priority: find a meeting that has already started but not yet ended
|
|
||||||
// (i.e. we're currently in a GP weekend).
|
|
||||||
for i := range m.meetings {
|
|
||||||
mtg := m.meetings[i]
|
|
||||||
start, _ := time.Parse(time.RFC3339, mtg.DateStart)
|
|
||||||
end, _ := time.Parse(time.RFC3339, mtg.DateEnd)
|
|
||||||
if now.After(start.Local()) && now.Before(end.Local().Add(24*time.Hour)) {
|
|
||||||
m.next = &mtg
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second priority: if no ongoing weekend, find the next upcoming meeting.
|
|
||||||
if m.next == nil {
|
if m.next == nil {
|
||||||
for i := range m.meetings {
|
m.next = nextUpcomingMeeting(m.meetings, now)
|
||||||
mtg := m.meetings[i]
|
|
||||||
start, _ := time.Parse(time.RFC3339, mtg.DateStart)
|
|
||||||
if now.Before(start.Local()) {
|
|
||||||
m.next = &mtg
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.next != nil {
|
if m.next != nil {
|
||||||
@@ -193,11 +169,8 @@ func (m DashboardModel) View() string {
|
|||||||
Foreground(lipgloss.Color(colorF1Red)).
|
Foreground(lipgloss.Color(colorF1Red)).
|
||||||
Bold(true)
|
Bold(true)
|
||||||
|
|
||||||
// Determine if the weekend has started
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
meetingStart, _ := time.Parse(time.RFC3339, m.next.DateStart)
|
weekendUnderway := meetingHasStarted(*m.next, now)
|
||||||
meetingStart = meetingStart.Local()
|
|
||||||
weekendUnderway := now.After(meetingStart)
|
|
||||||
|
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
if weekendUnderway {
|
if weekendUnderway {
|
||||||
@@ -207,42 +180,23 @@ func (m DashboardModel) View() string {
|
|||||||
}
|
}
|
||||||
sb.WriteString(fmt.Sprintf(" %s • %s\n", countryFlag(m.next.CountryCode), m.next.Location))
|
sb.WriteString(fmt.Sprintf(" %s • %s\n", countryFlag(m.next.CountryCode), m.next.Location))
|
||||||
|
|
||||||
end, _ := time.Parse(time.RFC3339, m.next.DateEnd)
|
currentSession, nextSession := currentAndNextSession(m.sessions, now)
|
||||||
endLocal := end.Local()
|
|
||||||
|
|
||||||
// Show countdown to next session if available, else use start date
|
|
||||||
var nextStart time.Time
|
|
||||||
var nextSession *models.Session
|
|
||||||
startFound := false
|
|
||||||
for i := range m.sessions {
|
|
||||||
st, _ := time.Parse(time.RFC3339, m.sessions[i].DateStart)
|
|
||||||
if now.Before(st.Local()) {
|
|
||||||
nextStart = st.Local()
|
|
||||||
nextSession = &m.sessions[i]
|
|
||||||
startFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !startFound {
|
|
||||||
nextStart, _ = time.Parse(time.RFC3339, m.next.DateStart)
|
|
||||||
nextStart = nextStart.Local()
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
if now.After(nextStart) && now.Before(endLocal) {
|
if currentSession != nil {
|
||||||
liveBadge := lipgloss.NewStyle().Background(lipgloss.Color(colorSoft)).Foreground(lipgloss.Color(colorSurface0)).Bold(true).Padding(0, 1).Render("LIVE")
|
liveBadge := lipgloss.NewStyle().Background(lipgloss.Color(colorSoft)).Foreground(lipgloss.Color(colorSurface0)).Bold(true).Padding(0, 1).Render("LIVE")
|
||||||
sb.WriteString(" " + liveBadge + "\n")
|
sb.WriteString(fmt.Sprintf(" %s %s\n", liveBadge, currentSession.SessionName))
|
||||||
} else if now.Before(nextStart) {
|
} else if nextSession != nil {
|
||||||
|
nextStart, err := sessionStartTime(*nextSession)
|
||||||
|
if err != nil {
|
||||||
|
sb.WriteString(" " + lipgloss.NewStyle().Foreground(lipgloss.Color(colorMuted)).Render("Schedule unavailable") + "\n")
|
||||||
|
} else {
|
||||||
diff := nextStart.Sub(now)
|
diff := nextStart.Sub(now)
|
||||||
days := int(diff.Hours() / 24)
|
days := int(diff.Hours() / 24)
|
||||||
hours := int(diff.Hours()) % 24
|
hours := int(diff.Hours()) % 24
|
||||||
mins := int(diff.Minutes()) % 60
|
mins := int(diff.Minutes()) % 60
|
||||||
secs := int(diff.Seconds()) % 60
|
secs := int(diff.Seconds()) % 60
|
||||||
if nextSession != nil {
|
|
||||||
sb.WriteString(fmt.Sprintf(" Next: %s in %dd %02dh %02dm %02ds\n", nextSession.SessionName, days, hours, mins, secs))
|
sb.WriteString(fmt.Sprintf(" Next: %s in %dd %02dh %02dm %02ds\n", nextSession.SessionName, days, hours, mins, secs))
|
||||||
} else {
|
|
||||||
sb.WriteString(fmt.Sprintf(" Starts in: %dd %02dh %02dm %02ds\n", days, hours, mins, secs))
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sb.WriteString(" " + lipgloss.NewStyle().Foreground(lipgloss.Color(colorMuted)).Render("Weekend Finished") + "\n")
|
sb.WriteString(" " + lipgloss.NewStyle().Foreground(lipgloss.Color(colorMuted)).Render("Weekend Finished") + "\n")
|
||||||
@@ -254,14 +208,18 @@ func (m DashboardModel) View() string {
|
|||||||
if len(m.sessions) > 0 {
|
if len(m.sessions) > 0 {
|
||||||
sb.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color(colorWhite)).Bold(true).Render(" WEEKEND SCHEDULE (Local Time)") + "\n")
|
sb.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color(colorWhite)).Bold(true).Render(" WEEKEND SCHEDULE (Local Time)") + "\n")
|
||||||
for _, s := range m.sessions {
|
for _, s := range m.sessions {
|
||||||
st, _ := time.Parse(time.RFC3339, s.DateStart)
|
stLocal, err := sessionStartTime(s)
|
||||||
stLocal := st.Local()
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
day := stLocal.Format("Mon 02 Jan")
|
day := stLocal.Format("Mon 02 Jan")
|
||||||
tStr := stLocal.Format("15:04")
|
tStr := stLocal.Format("15:04")
|
||||||
|
|
||||||
rowStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(colorWhite))
|
rowStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(colorWhite))
|
||||||
if now.After(stLocal) {
|
if end, err := sessionEndTime(s); err == nil && !now.Before(end) {
|
||||||
rowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(colorMuted))
|
rowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(colorMuted))
|
||||||
|
} else if currentSession != nil && currentSession.SessionKey == s.SessionKey {
|
||||||
|
rowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(colorSoft)).Bold(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.WriteString(rowStyle.Render(fmt.Sprintf(" %-15s %-12s %s", s.SessionName, day, tStr)) + "\n")
|
sb.WriteString(rowStyle.Render(fmt.Sprintf(" %-15s %-12s %s", s.SessionName, day, tStr)) + "\n")
|
||||||
|
|||||||
140
internal/ui/schedule.go
Normal file
140
internal/ui/schedule.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AmanTahiliani/box-box/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultSessionDuration = 3 * time.Hour
|
||||||
|
|
||||||
|
func parseScheduleTime(value string) (time.Time, error) {
|
||||||
|
if value == "" {
|
||||||
|
return time.Time{}, fmt.Errorf("empty time value")
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := time.Parse(time.RFC3339, value)
|
||||||
|
if err == nil {
|
||||||
|
return t.Local(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err = time.Parse("2006-01-02", value[:min(len(value), 10)])
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
return t.Local(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func meetingStartTime(meeting models.Meeting) (time.Time, error) {
|
||||||
|
return parseScheduleTime(meeting.DateStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
func meetingEndTime(meeting models.Meeting) (time.Time, error) {
|
||||||
|
if meeting.DateEnd != "" {
|
||||||
|
if end, err := parseScheduleTime(meeting.DateEnd); err == nil {
|
||||||
|
return end, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start, err := meetingStartTime(meeting)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
return start.Add(72 * time.Hour), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessionStartTime(session models.Session) (time.Time, error) {
|
||||||
|
return parseScheduleTime(session.DateStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessionEndTime(session models.Session) (time.Time, error) {
|
||||||
|
if session.DateEnd != "" {
|
||||||
|
if end, err := parseScheduleTime(session.DateEnd); err == nil {
|
||||||
|
return end, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start, err := sessionStartTime(session)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
return start.Add(defaultSessionDuration), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortSessionsByStart(sessions []models.Session) {
|
||||||
|
sort.Slice(sessions, func(i, j int) bool {
|
||||||
|
left, errLeft := sessionStartTime(sessions[i])
|
||||||
|
right, errRight := sessionStartTime(sessions[j])
|
||||||
|
if errLeft != nil || errRight != nil {
|
||||||
|
return sessions[i].DateStart < sessions[j].DateStart
|
||||||
|
}
|
||||||
|
return left.Before(right)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentMeeting(meetings []models.Meeting, now time.Time) *models.Meeting {
|
||||||
|
var selected *models.Meeting
|
||||||
|
var latest time.Time
|
||||||
|
|
||||||
|
for i := range meetings {
|
||||||
|
start, err := meetingStartTime(meetings[i])
|
||||||
|
if err != nil || start.After(now) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
end, err := meetingEndTime(meetings[i])
|
||||||
|
if err != nil || now.After(end.Add(24*time.Hour)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if selected == nil || start.After(latest) {
|
||||||
|
selected = &meetings[i]
|
||||||
|
latest = start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextUpcomingMeeting(meetings []models.Meeting, now time.Time) *models.Meeting {
|
||||||
|
for i := range meetings {
|
||||||
|
start, err := meetingStartTime(meetings[i])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if start.After(now) {
|
||||||
|
return &meetings[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentAndNextSession(sessions []models.Session, now time.Time) (*models.Session, *models.Session) {
|
||||||
|
for i := range sessions {
|
||||||
|
start, err := sessionStartTime(sessions[i])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
end, err := sessionEndTime(sessions[i])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now.Equal(start) || now.After(start)) && now.Before(end) {
|
||||||
|
return &sessions[i], nil
|
||||||
|
}
|
||||||
|
if now.Before(start) {
|
||||||
|
return nil, &sessions[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func meetingHasStarted(meeting models.Meeting, now time.Time) bool {
|
||||||
|
start, err := meetingStartTime(meeting)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !now.Before(start)
|
||||||
|
}
|
||||||
@@ -19,6 +19,53 @@ function currentSeason() {
|
|||||||
return new Date().getFullYear();
|
return new Date().getFullYear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseScheduleTime(value) {
|
||||||
|
const date = new Date(value);
|
||||||
|
return Number.isNaN(date.getTime()) ? null : date;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortSessionsByStart(sessions) {
|
||||||
|
return [...sessions].sort((a, b) => {
|
||||||
|
const left = parseScheduleTime(a.date_start);
|
||||||
|
const right = parseScheduleTime(b.date_start);
|
||||||
|
return (left?.getTime() || 0) - (right?.getTime() || 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickRelevantMeeting(meetings, now = new Date()) {
|
||||||
|
const started = meetings
|
||||||
|
.map(meeting => ({
|
||||||
|
meeting,
|
||||||
|
start: parseScheduleTime(meeting.date_start),
|
||||||
|
end: parseScheduleTime(meeting.date_end) || null,
|
||||||
|
}))
|
||||||
|
.filter(entry => entry.start && entry.start.getTime() <= now.getTime())
|
||||||
|
.filter(entry => !entry.end || now.getTime() <= entry.end.getTime() + 24 * 60 * 60 * 1000)
|
||||||
|
.sort((a, b) => b.start.getTime() - a.start.getTime());
|
||||||
|
if (started.length) return started[0].meeting;
|
||||||
|
|
||||||
|
return meetings.find(meeting => {
|
||||||
|
const start = parseScheduleTime(meeting.date_start);
|
||||||
|
return start && start.getTime() > now.getTime();
|
||||||
|
}) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickCurrentAndNextSession(sessions, now = new Date()) {
|
||||||
|
for (const session of sessions) {
|
||||||
|
const start = parseScheduleTime(session.date_start);
|
||||||
|
const end = parseScheduleTime(session.date_end) || (start ? new Date(start.getTime() + 3 * 60 * 60 * 1000) : null);
|
||||||
|
if (!start || !end) continue;
|
||||||
|
|
||||||
|
if (now.getTime() >= start.getTime() && now.getTime() < end.getTime()) {
|
||||||
|
return { current: session, next: null };
|
||||||
|
}
|
||||||
|
if (now.getTime() < start.getTime()) {
|
||||||
|
return { current: null, next: session };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { current: null, next: null };
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Root state: routing + live check ----
|
// ---- Root state: routing + live check ----
|
||||||
|
|
||||||
function appState() {
|
function appState() {
|
||||||
@@ -69,6 +116,10 @@ function homePage() {
|
|||||||
meetings: [],
|
meetings: [],
|
||||||
champDrivers: [],
|
champDrivers: [],
|
||||||
nextRace: null,
|
nextRace: null,
|
||||||
|
nextSession: null,
|
||||||
|
currentSession: null,
|
||||||
|
heroLabel: 'NEXT RACE',
|
||||||
|
heroMeta: '',
|
||||||
countdown: '',
|
countdown: '',
|
||||||
season: currentSeason(),
|
season: currentSeason(),
|
||||||
_countdownTimer: null,
|
_countdownTimer: null,
|
||||||
@@ -88,9 +139,33 @@ function homePage() {
|
|||||||
const r = await fetch(`/api/v1/meetings?year=${this.season}`);
|
const r = await fetch(`/api/v1/meetings?year=${this.season}`);
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
this.meetings = Array.isArray(data) ? data : [];
|
this.meetings = Array.isArray(data) ? data : [];
|
||||||
// Find next race
|
|
||||||
const now = Date.now();
|
const meeting = pickRelevantMeeting(this.meetings);
|
||||||
this.nextRace = this.meetings.find(m => new Date(m.date_start).getTime() > now) || null;
|
this.nextRace = meeting;
|
||||||
|
this.nextSession = null;
|
||||||
|
this.currentSession = null;
|
||||||
|
this.heroLabel = meeting && parseScheduleTime(meeting.date_start)?.getTime() <= Date.now()
|
||||||
|
? 'CURRENT WEEKEND'
|
||||||
|
: 'NEXT RACE';
|
||||||
|
this.heroMeta = '';
|
||||||
|
|
||||||
|
if (!meeting) return;
|
||||||
|
|
||||||
|
const sessionsResp = await fetch(`/api/v1/sessions?meeting_key=${meeting.meeting_key}`);
|
||||||
|
const sessionsData = await sessionsResp.json();
|
||||||
|
const sessions = Array.isArray(sessionsData) ? sortSessionsByStart(sessionsData) : [];
|
||||||
|
const { current, next } = pickCurrentAndNextSession(sessions);
|
||||||
|
|
||||||
|
this.currentSession = current;
|
||||||
|
this.nextSession = next;
|
||||||
|
|
||||||
|
if (current) {
|
||||||
|
this.heroLabel = 'LIVE NOW';
|
||||||
|
this.heroMeta = current.session_name;
|
||||||
|
} else if (next) {
|
||||||
|
this.heroLabel = 'NEXT SESSION';
|
||||||
|
this.heroMeta = next.session_name;
|
||||||
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -106,20 +181,46 @@ function homePage() {
|
|||||||
|
|
||||||
startCountdown() {
|
startCountdown() {
|
||||||
if (!this.nextRace) return;
|
if (!this.nextRace) return;
|
||||||
const target = new Date(this.nextRace.date_start).getTime();
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
const diff = target - Date.now();
|
const targetSource = this.currentSession || this.nextSession || this.nextRace;
|
||||||
if (diff <= 0) { this.countdown = 'Race day!'; return; }
|
const targetTime = this.currentSession
|
||||||
|
? parseScheduleTime(this.currentSession.date_end)?.getTime()
|
||||||
|
: parseScheduleTime(targetSource?.date_start)?.getTime();
|
||||||
|
if (!targetTime) {
|
||||||
|
this.countdown = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const diff = targetTime - Date.now();
|
||||||
|
if (this.currentSession) {
|
||||||
|
if (diff <= 0) {
|
||||||
|
this.countdown = 'Session complete';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.countdown = `Ends in ${this.formatDuration(diff)}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diff <= 0) {
|
||||||
|
this.countdown = this.nextSession ? 'Session starting' : 'Race day!';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = this.nextSession ? 'Starts in ' : '';
|
||||||
|
this.countdown = prefix + this.formatDuration(diff);
|
||||||
|
};
|
||||||
|
update();
|
||||||
|
this._countdownTimer = setInterval(update, 1000);
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDuration(diff) {
|
||||||
const d = Math.floor(diff / 86400000);
|
const d = Math.floor(diff / 86400000);
|
||||||
const h = Math.floor((diff % 86400000) / 3600000);
|
const h = Math.floor((diff % 86400000) / 3600000);
|
||||||
const m = Math.floor((diff % 3600000) / 60000);
|
const m = Math.floor((diff % 3600000) / 60000);
|
||||||
const s = Math.floor((diff % 60000) / 1000);
|
const s = Math.floor((diff % 60000) / 1000);
|
||||||
this.countdown = d > 0
|
return d > 0
|
||||||
? `${d}d ${pad(h)}:${pad(m)}:${pad(s)}`
|
? `${d}d ${pad(h)}:${pad(m)}:${pad(s)}`
|
||||||
: `${pad(h)}:${pad(m)}:${pad(s)}`;
|
: `${pad(h)}:${pad(m)}:${pad(s)}`;
|
||||||
};
|
|
||||||
update();
|
|
||||||
this._countdownTimer = setInterval(update, 1000);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
barWidth(pts) {
|
barWidth(pts) {
|
||||||
|
|||||||
@@ -40,10 +40,11 @@
|
|||||||
<div class="home-hero" x-show="!loading">
|
<div class="home-hero" x-show="!loading">
|
||||||
<!-- Next race -->
|
<!-- Next race -->
|
||||||
<div class="card next-race" x-show="nextRace">
|
<div class="card next-race" x-show="nextRace">
|
||||||
<div class="card-label">NEXT RACE</div>
|
<div class="card-label" x-text="heroLabel"></div>
|
||||||
<div class="next-race-flag" x-text="flagEmoji(nextRace?.country_code)"></div>
|
<div class="next-race-flag" x-text="flagEmoji(nextRace?.country_code)"></div>
|
||||||
<h2 class="next-race-name" x-text="nextRace?.meeting_name||''"></h2>
|
<h2 class="next-race-name" x-text="nextRace?.meeting_name||''"></h2>
|
||||||
<div class="next-race-circuit" x-text="nextRace?.circuit_short_name||''"></div>
|
<div class="next-race-circuit" x-text="nextRace?.circuit_short_name||''"></div>
|
||||||
|
<div class="next-race-session" x-show="heroMeta" x-text="heroMeta"></div>
|
||||||
<div class="next-race-countdown" x-text="countdown"></div>
|
<div class="next-race-countdown" x-text="countdown"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,13 @@ a { color: inherit; text-decoration: none; }
|
|||||||
.next-race-flag { font-size: 3rem; margin: 0.25rem 0; }
|
.next-race-flag { font-size: 3rem; margin: 0.25rem 0; }
|
||||||
.next-race-name { font-size: 1.4rem; font-weight: 700; margin: 0.25rem 0; }
|
.next-race-name { font-size: 1.4rem; font-weight: 700; margin: 0.25rem 0; }
|
||||||
.next-race-circuit { color: var(--text-2); font-size: 0.85rem; margin-bottom: 0.5rem; }
|
.next-race-circuit { color: var(--text-2); font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||||||
|
.next-race-session {
|
||||||
|
color: var(--text-1);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
margin-bottom: 0.45rem;
|
||||||
|
}
|
||||||
.next-race-countdown {
|
.next-race-countdown {
|
||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
|
|||||||
Reference in New Issue
Block a user