diff --git a/db/db.go b/db/db.go
index e7556ea..defd07a 100644
--- a/db/db.go
+++ b/db/db.go
@@ -2,6 +2,7 @@ package db
import (
"database/sql"
+ "errors"
"fmt"
"log/slog"
"os"
@@ -11,6 +12,9 @@ import (
_ "github.com/mattn/go-sqlite3"
)
+// ErrNotFound is returned when a requested record is not found
+var ErrNotFound = errors.New("record not found")
+
// DB represents the database connection
type DB struct {
*sql.DB
@@ -134,6 +138,46 @@ func (db *DB) initSchema() error {
return fmt.Errorf("failed to create heats table: %w", err)
}
+ // Create settings table
+ _, err = db.Exec(`
+ CREATE TABLE IF NOT EXISTS settings (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL
+ );
+ `)
+ if err != nil {
+ return fmt.Errorf("failed to create settings table: %w", err)
+ }
+
+ // Create heat_results table
+ _, err = db.Exec(`
+ CREATE TABLE IF NOT EXISTS heat_results (
+ group_id INTEGER NOT NULL,
+ heat_number INTEGER NOT NULL,
+ lane1_time REAL NOT NULL,
+ lane1_position INTEGER NOT NULL,
+ lane2_time REAL NOT NULL,
+ lane2_position INTEGER NOT NULL,
+ lane3_time REAL NOT NULL,
+ lane3_position INTEGER NOT NULL,
+ lane4_time REAL NOT NULL,
+ lane4_position INTEGER NOT NULL,
+ PRIMARY KEY (group_id, heat_number),
+ FOREIGN KEY (group_id) REFERENCES groups (id) ON DELETE CASCADE
+ );
+ `)
+ if err != nil {
+ return fmt.Errorf("failed to create heat_results table: %w", err)
+ }
+
+ // Add default settings if they don't exist
+ _, err = db.Exec(`
+ INSERT OR IGNORE INTO settings (key, value) VALUES ('current_racing_group', '1');
+ `)
+ if err != nil {
+ return fmt.Errorf("failed to add default settings: %w", err)
+ }
+
return nil
}
@@ -220,3 +264,180 @@ func (db *DB) DeleteHeats(groupID int64) error {
_, err := db.Exec("DELETE FROM heats WHERE group_id = ?", groupID)
return err
}
+
+// GetCurrentRacingGroup gets the currently racing group
+func (db *DB) GetCurrentRacingGroup() (models.Group, error) {
+ var group models.Group
+
+ // Get the current racing group ID from settings
+ var groupID int64
+ err := db.QueryRow("SELECT value FROM settings WHERE key = 'current_racing_group'").Scan(&groupID)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ // If no group is set, get the first group
+ err = db.QueryRow("SELECT id, name, description FROM groups ORDER BY id LIMIT 1").Scan(
+ &group.ID, &group.Name, &group.Description)
+ if err != nil {
+ return group, err
+ }
+
+ // Set this as the current racing group
+ _, err = db.Exec("INSERT OR REPLACE INTO settings (key, value) VALUES ('current_racing_group', ?)", group.ID)
+ return group, err
+ }
+ return group, err
+ }
+
+ // Get the group details
+ err = db.QueryRow("SELECT id, name, description FROM groups WHERE id = ?", groupID).Scan(
+ &group.ID, &group.Name, &group.Description)
+ return group, err
+}
+
+// SetCurrentRacingGroup sets the currently racing group
+func (db *DB) SetCurrentRacingGroup(groupID int64) error {
+ _, err := db.Exec("INSERT OR REPLACE INTO settings (key, value) VALUES ('current_racing_group', ?)", groupID)
+ return err
+}
+
+// GetCurrentHeatNumber gets the current heat number for a group
+func (db *DB) GetCurrentHeatNumber(groupID int64) (int, error) {
+ var heatNum int
+ key := fmt.Sprintf("current_heat_%d", groupID)
+
+ err := db.QueryRow("SELECT value FROM settings WHERE key = ?", key).Scan(&heatNum)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ // If no heat is set, default to 1
+ _, err = db.Exec("INSERT INTO settings (key, value) VALUES (?, 1)", key)
+ return 1, err
+ }
+ return 0, err
+ }
+
+ return heatNum, nil
+}
+
+// SetCurrentHeatNumber sets the current heat number for a group
+func (db *DB) SetCurrentHeatNumber(groupID int64, heatNum int) error {
+ key := fmt.Sprintf("current_heat_%d", groupID)
+ _, err := db.Exec("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", key, heatNum)
+ return err
+}
+
+// SaveHeatResult saves the result of a heat
+func (db *DB) SaveHeatResult(result models.HeatResult) error {
+ // Check if result already exists
+ var count int
+ err := db.QueryRow("SELECT COUNT(*) FROM heat_results WHERE group_id = ? AND heat_number = ?",
+ result.GroupID, result.HeatNumber).Scan(&count)
+ if err != nil {
+ return err
+ }
+
+ if count > 0 {
+ // Update existing result
+ _, err = db.Exec(`
+ UPDATE heat_results SET
+ lane1_time = ?, lane1_position = ?,
+ lane2_time = ?, lane2_position = ?,
+ lane3_time = ?, lane3_position = ?,
+ lane4_time = ?, lane4_position = ?
+ WHERE group_id = ? AND heat_number = ?`,
+ result.Lane1Time, result.Lane1Position,
+ result.Lane2Time, result.Lane2Position,
+ result.Lane3Time, result.Lane3Position,
+ result.Lane4Time, result.Lane4Position,
+ result.GroupID, result.HeatNumber)
+ } else {
+ // Insert new result
+ _, err = db.Exec(`
+ INSERT INTO heat_results (
+ group_id, heat_number,
+ lane1_time, lane1_position,
+ lane2_time, lane2_position,
+ lane3_time, lane3_position,
+ lane4_time, lane4_position
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ result.GroupID, result.HeatNumber,
+ result.Lane1Time, result.Lane1Position,
+ result.Lane2Time, result.Lane2Position,
+ result.Lane3Time, result.Lane3Position,
+ result.Lane4Time, result.Lane4Position)
+ }
+
+ return err
+}
+
+// GetHeatResult gets the result of a specific heat
+func (db *DB) GetHeatResult(groupID int64, heatNum int) (*models.HeatResult, error) {
+ result := &models.HeatResult{}
+
+ err := db.QueryRow(`
+ SELECT
+ group_id, heat_number,
+ lane1_time, lane1_position,
+ lane2_time, lane2_position,
+ lane3_time, lane3_position,
+ lane4_time, lane4_position
+ FROM heat_results
+ WHERE group_id = ? AND heat_number = ?`,
+ groupID, heatNum).Scan(
+ &result.GroupID, &result.HeatNumber,
+ &result.Lane1Time, &result.Lane1Position,
+ &result.Lane2Time, &result.Lane2Position,
+ &result.Lane3Time, &result.Lane3Position,
+ &result.Lane4Time, &result.Lane4Position)
+
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil, ErrNotFound
+ }
+ return nil, err
+ }
+
+ return result, nil
+}
+
+// GetHeatResults gets all heat results for a group
+func (db *DB) GetHeatResults(groupID int64) ([]models.HeatResult, error) {
+ rows, err := db.Query(`
+ SELECT
+ group_id, heat_number,
+ lane1_time, lane1_position,
+ lane2_time, lane2_position,
+ lane3_time, lane3_position,
+ lane4_time, lane4_position
+ FROM heat_results
+ WHERE group_id = ?
+ ORDER BY heat_number`,
+ groupID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var results []models.HeatResult
+ for rows.Next() {
+ var result models.HeatResult
+ err := rows.Scan(
+ &result.GroupID, &result.HeatNumber,
+ &result.Lane1Time, &result.Lane1Position,
+ &result.Lane2Time, &result.Lane2Position,
+ &result.Lane3Time, &result.Lane3Position,
+ &result.Lane4Time, &result.Lane4Position)
+ if err != nil {
+ return nil, err
+ }
+ results = append(results, result)
+ }
+
+ return results, nil
+}
+
+// DeleteHeatResult deletes a heat result
+func (db *DB) DeleteHeatResult(groupID int64, heatNum int) error {
+ _, err := db.Exec("DELETE FROM heat_results WHERE group_id = ? AND heat_number = ?",
+ groupID, heatNum)
+ return err
+}
diff --git a/models/models.go b/models/models.go
index ef5386f..c003c65 100644
--- a/models/models.go
+++ b/models/models.go
@@ -53,3 +53,17 @@ type Heat struct {
Lane3ID *int64 `json:"lane3_id"`
Lane4ID *int64 `json:"lane4_id"`
}
+
+// HeatResult represents the result of a heat
+type HeatResult struct {
+ GroupID int64 `json:"group_id"`
+ HeatNumber int `json:"heat_number"`
+ Lane1Time float64 `json:"lane1_time"`
+ Lane1Position int `json:"lane1_position"`
+ Lane2Time float64 `json:"lane2_time"`
+ Lane2Position int `json:"lane2_position"`
+ Lane3Time float64 `json:"lane3_time"`
+ Lane3Position int `json:"lane3_position"`
+ Lane4Time float64 `json:"lane4_time"`
+ Lane4Position int `json:"lane4_position"`
+}
diff --git a/web/server.go b/web/server.go
index 02b5ed7..3d609f8 100644
--- a/web/server.go
+++ b/web/server.go
@@ -153,6 +153,20 @@ func (s *Server) routes() {
// Add validate car number route
s.router.Get("/api/validate/car-number", s.handleValidateCarNumber())
+
+ // Add race manager routes
+ s.router.Get("/race", s.handleRacePublic())
+ s.router.Get("/race/manage", s.handleRaceManage())
+
+ // Add race API routes
+ s.router.Route("/api/race", func(r chi.Router) {
+ r.Get("/current-heat", s.handleCurrentHeat())
+ r.Post("/next-heat", s.handleNextHeat())
+ r.Post("/previous-heat", s.handlePreviousHeat())
+ r.Post("/save-result", s.handleSaveHeatResult())
+ r.Post("/rerun-heat", s.handleRerunHeat())
+ r.Post("/set-group", s.handleSetRacingGroup())
+ })
}
// Start starts the web server
@@ -991,3 +1005,503 @@ func (s *Server) handleValidateCarNumber() http.HandlerFunc {
}
}
}
+
+// handleRacePublic renders the public race view page
+func (s *Server) handleRacePublic() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Get the currently racing group
+ currentGroup, err := s.db.GetCurrentRacingGroup()
+ if err != nil {
+ s.logger.Error("Failed to get current racing group", "error", err)
+ http.Error(w, "Failed to get current racing group", http.StatusInternalServerError)
+ return
+ }
+
+ // Get current heat number
+ currentHeatNum, err := s.db.GetCurrentHeatNumber(currentGroup.ID)
+ if err != nil {
+ s.logger.Error("Failed to get current heat number", "error", err)
+ http.Error(w, "Failed to get current heat number", http.StatusInternalServerError)
+ return
+ }
+
+ // Get heats for the group
+ heats, err := s.db.GetHeats(currentGroup.ID)
+ if err != nil {
+ s.logger.Error("Failed to get heats", "error", err)
+ http.Error(w, "Failed to get heats", http.StatusInternalServerError)
+ return
+ }
+
+ // Get racers for the group
+ racers, err := s.db.GetRacersByGroup(currentGroup.ID)
+ if err != nil {
+ s.logger.Error("Failed to get racers", "error", err)
+ http.Error(w, "Failed to get racers", http.StatusInternalServerError)
+ return
+ }
+
+ // Get heat results
+ results, err := s.db.GetHeatResults(currentGroup.ID)
+ if err != nil {
+ s.logger.Error("Failed to get heat results", "error", err)
+ http.Error(w, "Failed to get heat results", http.StatusInternalServerError)
+ return
+ }
+
+ // Render template
+ component := templates.RacePublic(currentGroup, heats, racers, currentHeatNum, results)
+ if err := component.Render(r.Context(), w); err != nil {
+ s.logger.Error("Failed to render race public template", "error", err)
+ http.Error(w, "Failed to render page", http.StatusInternalServerError)
+ }
+ }
+}
+
+// handleRaceManage renders the race management page
+func (s *Server) handleRaceManage() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Get all groups
+ groups, err := s.db.GetGroups()
+ if err != nil {
+ s.logger.Error("Failed to get groups", "error", err)
+ http.Error(w, "Failed to get groups", http.StatusInternalServerError)
+ return
+ }
+
+ // Get the currently racing group
+ currentGroup, err := s.db.GetCurrentRacingGroup()
+ if err != nil {
+ s.logger.Error("Failed to get current racing group", "error", err)
+ http.Error(w, "Failed to get current racing group", http.StatusInternalServerError)
+ return
+ }
+
+ // Get current heat number
+ currentHeatNum, err := s.db.GetCurrentHeatNumber(currentGroup.ID)
+ if err != nil {
+ s.logger.Error("Failed to get current heat number", "error", err)
+ http.Error(w, "Failed to get current heat number", http.StatusInternalServerError)
+ return
+ }
+
+ // Get heats for the group
+ heats, err := s.db.GetHeats(currentGroup.ID)
+ if err != nil {
+ s.logger.Error("Failed to get heats", "error", err)
+ http.Error(w, "Failed to get heats", http.StatusInternalServerError)
+ return
+ }
+
+ // Get racers for the group
+ racers, err := s.db.GetRacersByGroup(currentGroup.ID)
+ if err != nil {
+ s.logger.Error("Failed to get racers", "error", err)
+ http.Error(w, "Failed to get racers", http.StatusInternalServerError)
+ return
+ }
+
+ // Get heat results
+ results, err := s.db.GetHeatResults(currentGroup.ID)
+ if err != nil {
+ s.logger.Error("Failed to get heat results", "error", err)
+ http.Error(w, "Failed to get heat results", http.StatusInternalServerError)
+ return
+ }
+
+ // Render template
+ component := templates.RaceManage(groups, currentGroup, heats, racers, currentHeatNum, results)
+ if err := component.Render(r.Context(), w); err != nil {
+ s.logger.Error("Failed to render race manage template", "error", err)
+ http.Error(w, "Failed to render page", http.StatusInternalServerError)
+ }
+ }
+}
+
+// API handlers for race management
+
+// handleCurrentHeat returns the current heat data
+func (s *Server) handleCurrentHeat() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Get the currently racing group
+ currentGroup, err := s.db.GetCurrentRacingGroup()
+ if err != nil {
+ s.logger.Error("Failed to get current racing group", "error", err)
+ http.Error(w, "Failed to get current racing group", http.StatusInternalServerError)
+ return
+ }
+
+ // Get current heat number
+ currentHeatNum, err := s.db.GetCurrentHeatNumber(currentGroup.ID)
+ if err != nil {
+ s.logger.Error("Failed to get current heat number", "error", err)
+ http.Error(w, "Failed to get current heat number", http.StatusInternalServerError)
+ return
+ }
+
+ // Get heats for the group
+ heats, err := s.db.GetHeats(currentGroup.ID)
+ if err != nil {
+ s.logger.Error("Failed to get heats", "error", err)
+ http.Error(w, "Failed to get heats", http.StatusInternalServerError)
+ return
+ }
+
+ // Get racers for the group
+ racers, err := s.db.GetRacersByGroup(currentGroup.ID)
+ if err != nil {
+ s.logger.Error("Failed to get racers", "error", err)
+ http.Error(w, "Failed to get racers", http.StatusInternalServerError)
+ return
+ }
+
+ // Find current heat
+ var currentHeat models.Heat
+ for _, heat := range heats {
+ if heat.HeatNum == currentHeatNum {
+ currentHeat = heat
+ break
+ }
+ }
+
+ // Create response with racer details
+ type LaneInfo struct {
+ Lane int `json:"lane"`
+ RacerID int64 `json:"racer_id"`
+ Name string `json:"name"`
+ CarNum string `json:"car_number"`
+ Time interface{} `json:"time"`
+ Position interface{} `json:"position"`
+ }
+
+ response := struct {
+ HeatNumber int `json:"heat_number"`
+ TotalHeats int `json:"total_heats"`
+ GroupID int64 `json:"group_id"`
+ GroupName string `json:"group_name"`
+ Lanes []LaneInfo `json:"lanes"`
+ }{
+ HeatNumber: currentHeatNum,
+ TotalHeats: len(heats),
+ GroupID: currentGroup.ID,
+ GroupName: currentGroup.Name,
+ Lanes: make([]LaneInfo, 0),
+ }
+
+ // Get heat result if available
+ result, err := s.db.GetHeatResult(currentGroup.ID, currentHeatNum)
+ if err != nil && err != db.ErrNotFound {
+ s.logger.Error("Failed to get heat result", "error", err)
+ }
+
+ // Add lane 1 info
+ if currentHeat.Lane1ID != nil {
+ lane := LaneInfo{Lane: 1, RacerID: *currentHeat.Lane1ID}
+ for _, racer := range racers {
+ if racer.ID == *currentHeat.Lane1ID {
+ lane.Name = racer.FirstName + " " + racer.LastName
+ lane.CarNum = racer.CarNumber
+ break
+ }
+ }
+ if result != nil {
+ lane.Time = result.Lane1Time
+ lane.Position = result.Lane1Position
+ }
+ response.Lanes = append(response.Lanes, lane)
+ }
+
+ // Add lane 2 info
+ if currentHeat.Lane2ID != nil {
+ lane := LaneInfo{Lane: 2, RacerID: *currentHeat.Lane2ID}
+ for _, racer := range racers {
+ if racer.ID == *currentHeat.Lane2ID {
+ lane.Name = racer.FirstName + " " + racer.LastName
+ lane.CarNum = racer.CarNumber
+ break
+ }
+ }
+ if result != nil {
+ lane.Time = result.Lane2Time
+ lane.Position = result.Lane2Position
+ }
+ response.Lanes = append(response.Lanes, lane)
+ }
+
+ // Add lane 3 info
+ if currentHeat.Lane3ID != nil {
+ lane := LaneInfo{Lane: 3, RacerID: *currentHeat.Lane3ID}
+ for _, racer := range racers {
+ if racer.ID == *currentHeat.Lane3ID {
+ lane.Name = racer.FirstName + " " + racer.LastName
+ lane.CarNum = racer.CarNumber
+ break
+ }
+ }
+ if result != nil {
+ lane.Time = result.Lane3Time
+ lane.Position = result.Lane3Position
+ }
+ response.Lanes = append(response.Lanes, lane)
+ }
+
+ // Add lane 4 info
+ if currentHeat.Lane4ID != nil {
+ lane := LaneInfo{Lane: 4, RacerID: *currentHeat.Lane4ID}
+ for _, racer := range racers {
+ if racer.ID == *currentHeat.Lane4ID {
+ lane.Name = racer.FirstName + " " + racer.LastName
+ lane.CarNum = racer.CarNumber
+ break
+ }
+ }
+ if result != nil {
+ lane.Time = result.Lane4Time
+ lane.Position = result.Lane4Position
+ }
+ response.Lanes = append(response.Lanes, lane)
+ }
+
+ // Return JSON response
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(response)
+ }
+}
+
+// handleNextHeat advances to the next heat
+func (s *Server) handleNextHeat() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Get the currently racing group
+ currentGroup, err := s.db.GetCurrentRacingGroup()
+ if err != nil {
+ s.logger.Error("Failed to get current racing group", "error", err)
+ http.Error(w, "Failed to get current racing group", http.StatusInternalServerError)
+ return
+ }
+
+ // Get current heat number
+ currentHeatNum, err := s.db.GetCurrentHeatNumber(currentGroup.ID)
+ if err != nil {
+ s.logger.Error("Failed to get current heat number", "error", err)
+ http.Error(w, "Failed to get current heat number", http.StatusInternalServerError)
+ return
+ }
+
+ // Get heats for the group
+ heats, err := s.db.GetHeats(currentGroup.ID)
+ if err != nil {
+ s.logger.Error("Failed to get heats", "error", err)
+ http.Error(w, "Failed to get heats", http.StatusInternalServerError)
+ return
+ }
+
+ // Check if we're already at the last heat
+ if currentHeatNum >= len(heats) {
+ http.Error(w, "Already at the last heat", http.StatusBadRequest)
+ return
+ }
+
+ // Advance to next heat
+ if err := s.db.SetCurrentHeatNumber(currentGroup.ID, currentHeatNum+1); err != nil {
+ s.logger.Error("Failed to set current heat number", "error", err)
+ http.Error(w, "Failed to advance to next heat", http.StatusInternalServerError)
+ return
+ }
+
+ // Reset the clock
+ if err := s.clock.Reset(); err != nil {
+ s.logger.Error("Failed to reset clock", "error", err)
+ }
+
+ // Broadcast event to admin page
+ select {
+ case s.adminEvents <- "heat-changed":
+ // Event sent
+ default:
+ // Channel full, non-blocking
+ }
+
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(map[string]string{"status": "success"})
+ }
+}
+
+// handlePreviousHeat goes back to the previous heat
+func (s *Server) handlePreviousHeat() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Get the currently racing group
+ currentGroup, err := s.db.GetCurrentRacingGroup()
+ if err != nil {
+ s.logger.Error("Failed to get current racing group", "error", err)
+ http.Error(w, "Failed to get current racing group", http.StatusInternalServerError)
+ return
+ }
+
+ // Get current heat number
+ currentHeatNum, err := s.db.GetCurrentHeatNumber(currentGroup.ID)
+ if err != nil {
+ s.logger.Error("Failed to get current heat number", "error", err)
+ http.Error(w, "Failed to get current heat number", http.StatusInternalServerError)
+ return
+ }
+
+ // Check if we're already at the first heat
+ if currentHeatNum <= 1 {
+ http.Error(w, "Already at the first heat", http.StatusBadRequest)
+ return
+ }
+
+ // Go back to previous heat
+ if err := s.db.SetCurrentHeatNumber(currentGroup.ID, currentHeatNum-1); err != nil {
+ s.logger.Error("Failed to set current heat number", "error", err)
+ http.Error(w, "Failed to go back to previous heat", http.StatusInternalServerError)
+ return
+ }
+
+ // Reset the clock
+ if err := s.clock.Reset(); err != nil {
+ s.logger.Error("Failed to reset clock", "error", err)
+ }
+
+ // Broadcast event to admin page
+ select {
+ case s.adminEvents <- "heat-changed":
+ // Event sent
+ default:
+ // Channel full, non-blocking
+ }
+
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(map[string]string{"status": "success"})
+ }
+}
+
+// handleSaveHeatResult saves the result of a heat
+func (s *Server) handleSaveHeatResult() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Parse request body
+ var request struct {
+ GroupID int64 `json:"group_id"`
+ HeatNumber int `json:"heat_number"`
+ Lane1Time float64 `json:"lane1_time"`
+ Lane1Position int `json:"lane1_position"`
+ Lane2Time float64 `json:"lane2_time"`
+ Lane2Position int `json:"lane2_position"`
+ Lane3Time float64 `json:"lane3_time"`
+ Lane3Position int `json:"lane3_position"`
+ Lane4Time float64 `json:"lane4_time"`
+ Lane4Position int `json:"lane4_position"`
+ }
+
+ if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
+ http.Error(w, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ // Create heat result
+ result := models.HeatResult{
+ GroupID: request.GroupID,
+ HeatNumber: request.HeatNumber,
+ Lane1Time: request.Lane1Time,
+ Lane1Position: request.Lane1Position,
+ Lane2Time: request.Lane2Time,
+ Lane2Position: request.Lane2Position,
+ Lane3Time: request.Lane3Time,
+ Lane3Position: request.Lane3Position,
+ Lane4Time: request.Lane4Time,
+ Lane4Position: request.Lane4Position,
+ }
+
+ // Save heat result
+ if err := s.db.SaveHeatResult(result); err != nil {
+ s.logger.Error("Failed to save heat result", "error", err)
+ http.Error(w, "Failed to save heat result", http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(map[string]string{"status": "success"})
+ }
+}
+
+// handleRerunHeat marks a heat for rerun
+func (s *Server) handleRerunHeat() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Get the currently racing group
+ currentGroup, err := s.db.GetCurrentRacingGroup()
+ if err != nil {
+ s.logger.Error("Failed to get current racing group", "error", err)
+ http.Error(w, "Failed to get current racing group", http.StatusInternalServerError)
+ return
+ }
+
+ // Get current heat number
+ currentHeatNum, err := s.db.GetCurrentHeatNumber(currentGroup.ID)
+ if err != nil {
+ s.logger.Error("Failed to get current heat number", "error", err)
+ http.Error(w, "Failed to get current heat number", http.StatusInternalServerError)
+ return
+ }
+
+ // Delete any existing result for this heat
+ if err := s.db.DeleteHeatResult(currentGroup.ID, currentHeatNum); err != nil {
+ s.logger.Error("Failed to delete heat result", "error", err)
+ http.Error(w, "Failed to mark heat for rerun", http.StatusInternalServerError)
+ return
+ }
+
+ // Reset the clock
+ if err := s.clock.Reset(); err != nil {
+ s.logger.Error("Failed to reset clock", "error", err)
+ }
+
+ // Broadcast event to admin page
+ select {
+ case s.adminEvents <- "heat-rerun":
+ // Event sent
+ default:
+ // Channel full, non-blocking
+ }
+
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(map[string]string{"status": "success"})
+ }
+}
+
+// handleSetRacingGroup sets the currently racing group
+func (s *Server) handleSetRacingGroup() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Parse request body
+ var request struct {
+ GroupID int64 `json:"group_id"`
+ }
+
+ if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
+ http.Error(w, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ // Set current racing group
+ if err := s.db.SetCurrentRacingGroup(request.GroupID); err != nil {
+ s.logger.Error("Failed to set current racing group", "error", err)
+ http.Error(w, "Failed to set current racing group", http.StatusInternalServerError)
+ return
+ }
+
+ // Reset the clock
+ if err := s.clock.Reset(); err != nil {
+ s.logger.Error("Failed to reset clock", "error", err)
+ }
+
+ // Broadcast event to admin page
+ select {
+ case s.adminEvents <- "group-changed":
+ // Event sent
+ default:
+ // Channel full, non-blocking
+ }
+
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(map[string]string{"status": "success"})
+ }
+}
diff --git a/web/templates/race_manage.templ b/web/templates/race_manage.templ
new file mode 100644
index 0000000..2a35d6f
--- /dev/null
+++ b/web/templates/race_manage.templ
@@ -0,0 +1,449 @@
+package templates
+
+import (
+ "track-gopher/models"
+ "fmt"
+ "strconv"
+)
+
+// RaceManage renders the race management view
+templ RaceManage(groups []models.Group, currentGroup models.Group, heats []models.Heat, racers []models.Racer, currentHeatNum int, results []models.HeatResult) {
+ @Layout("Race Management") {
+
+
+
+
+
+
+
+ Racing Group
+
+ for _, group := range groups {
+
+ { group.Name }
+
+ }
+
+
+
+
+
+ Previous Heat
+
+
+ Heat { strconv.Itoa(currentHeatNum) } of { strconv.Itoa(len(heats)) }
+
+ = len(heats) }>
+ Next Heat
+
+
+
+
+
+ Reset Timer
+
+
+ Force End Heat
+
+
+ Re-run Heat
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Gate Status: Unknown
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @raceManageCurrentHeat(heats, racers, currentHeatNum, results)
+
+
+
+
+
+
+
+
+
+
+
+ if currentHeatNum < len(heats) {
+ @raceManageNextHeat(heats, racers, currentHeatNum+1)
+ } else {
+
No more heats in this group
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+ Heat
+ Lane 1
+ Lane 2
+ Lane 3
+ Lane 4
+
+
+
+ for _, result := range results {
+
+ { strconv.Itoa(result.HeatNumber) }
+ { fmt.Sprintf("%.3f (#%d)", result.Lane1Time, result.Lane1Position) }
+ { fmt.Sprintf("%.3f (#%d)", result.Lane2Time, result.Lane2Position) }
+ { fmt.Sprintf("%.3f (#%d)", result.Lane3Time, result.Lane3Position) }
+ { fmt.Sprintf("%.3f (#%d)", result.Lane4Time, result.Lane4Position) }
+
+ }
+
+
+
+
+
+
+
+
+
+
+ }
+}
+
+// Helper template for displaying current heat in the management view
+templ raceManageCurrentHeat(heats []models.Heat, racers []models.Racer, currentHeatNum int, results []models.HeatResult) {
+ {{
+ // Find current heat
+ var currentHeat models.Heat
+ for _, heat := range heats {
+ if heat.HeatNum == currentHeatNum {
+ currentHeat = heat
+ break
+ }
+ }
+
+ // Find heat result if available
+ var currentResult *models.HeatResult
+ for _, result := range results {
+ if result.HeatNumber == currentHeatNum {
+ currentResult = &result
+ break
+ }
+ }
+ }}
+
+
+
+
+
+ Lane
+ Racer
+ Car #
+ Time
+ Position
+
+
+
+ if currentHeat.Lane1ID != nil {
+ @raceManageLaneRow(1, *currentHeat.Lane1ID, racers, currentResult)
+ }
+ if currentHeat.Lane2ID != nil {
+ @raceManageLaneRow(2, *currentHeat.Lane2ID, racers, currentResult)
+ }
+ if currentHeat.Lane3ID != nil {
+ @raceManageLaneRow(3, *currentHeat.Lane3ID, racers, currentResult)
+ }
+ if currentHeat.Lane4ID != nil {
+ @raceManageLaneRow(4, *currentHeat.Lane4ID, racers, currentResult)
+ }
+
+
+
+}
+
+// Helper template for displaying a lane row in the management view
+templ raceManageLaneRow(lane int, racerID int64, racers []models.Racer, result *models.HeatResult) {
+ {{
+ // Find racer
+ var racer models.Racer
+ for _, r := range racers {
+ if r.ID == racerID {
+ racer = r
+ break
+ }
+ }
+
+ // Get time and position from result if available
+ var time float64
+ var position int
+ var hasResult bool
+
+ if result != nil {
+ hasResult = true
+ switch lane {
+ case 1:
+ time = result.Lane1Time
+ position = result.Lane1Position
+ case 2:
+ time = result.Lane2Time
+ position = result.Lane2Position
+ case 3:
+ time = result.Lane3Time
+ position = result.Lane3Position
+ case 4:
+ time = result.Lane4Time
+ position = result.Lane4Position
+ }
+ }
+ }}
+
+
+ { strconv.Itoa(lane) }
+ { racer.FirstName } { racer.LastName }
+ { racer.CarNumber }
+
+ if hasResult {
+ { fmt.Sprintf("%.3f", time) }
+ } else {
+ --.-
+ }
+
+
+ if hasResult {
+ { strconv.Itoa(position) }
+ } else {
+ -
+ }
+
+
+}
+
+// Helper template for displaying next heat in the management view
+templ raceManageNextHeat(heats []models.Heat, racers []models.Racer, heatNum int) {
+ {{
+ // Find the heat
+ var nextHeat models.Heat
+ for _, heat := range heats {
+ if heat.HeatNum == heatNum {
+ nextHeat = heat
+ break
+ }
+ }
+ }}
+
+
+
+
+
+ Lane
+ Racer
+ Car #
+
+
+
+ if nextHeat.Lane1ID != nil {
+ @raceManageNextHeatRow(1, *nextHeat.Lane1ID, racers)
+ }
+ if nextHeat.Lane2ID != nil {
+ @raceManageNextHeatRow(2, *nextHeat.Lane2ID, racers)
+ }
+ if nextHeat.Lane3ID != nil {
+ @raceManageNextHeatRow(3, *nextHeat.Lane3ID, racers)
+ }
+ if nextHeat.Lane4ID != nil {
+ @raceManageNextHeatRow(4, *nextHeat.Lane4ID, racers)
+ }
+
+
+
+}
+
+// Helper template for displaying a row in the next heat preview
+templ raceManageNextHeatRow(lane int, racerID int64, racers []models.Racer) {
+ {{
+ // Find racer
+ var racer models.Racer
+ for _, r := range racers {
+ if r.ID == racerID {
+ racer = r
+ break
+ }
+ }
+ }}
+
+
+ { strconv.Itoa(lane) }
+ { racer.FirstName } { racer.LastName }
+ { racer.CarNumber }
+
+}
\ No newline at end of file
diff --git a/web/templates/race_manage_templ.go b/web/templates/race_manage_templ.go
new file mode 100644
index 0000000..b88c7d9
--- /dev/null
+++ b/web/templates/race_manage_templ.go
@@ -0,0 +1,669 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.3.833
+package templates
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+import (
+ "fmt"
+ "strconv"
+ "track-gopher/models"
+)
+
+// RaceManage renders the race management view
+func RaceManage(groups []models.Group, currentGroup models.Group, heats []models.Heat, racers []models.Racer, currentHeatNum int, results []models.HeatResult) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "Racing Group ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, group := range groups {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(group.Name)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_manage.templ`, Line: 25, Col: 56}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
Previous Heat Heat ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var5 string
+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(currentHeatNum))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_manage.templ`, Line: 36, Col: 79}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " of ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var6 string
+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(len(heats)))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_manage.templ`, Line: 36, Col: 111}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " = len(heats) {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " disabled")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ">Next Heat
Reset Timer Force End Heat Re-run Heat
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = raceManageCurrentHeat(heats, racers, currentHeatNum, results).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if currentHeatNum < len(heats) {
+ templ_7745c5c3_Err = raceManageNextHeat(heats, racers, currentHeatNum+1).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
No more heats in this group
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
Heat Lane 1 Lane 2 Lane 3 Lane 4 ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, result := range results {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var7 string
+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(result.HeatNumber))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_manage.templ`, Line: 130, Col: 85}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var8 string
+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.3f (#%d)", result.Lane1Time, result.Lane1Position))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_manage.templ`, Line: 131, Col: 119}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var9 string
+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.3f (#%d)", result.Lane2Time, result.Lane2Position))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_manage.templ`, Line: 132, Col: 119}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var10 string
+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.3f (#%d)", result.Lane3Time, result.Lane3Position))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_manage.templ`, Line: 133, Col: 119}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var11 string
+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.3f (#%d)", result.Lane4Time, result.Lane4Position))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_manage.templ`, Line: 134, Col: 119}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = Layout("Race Management").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Helper template for displaying current heat in the management view
+func raceManageCurrentHeat(heats []models.Heat, racers []models.Racer, currentHeatNum int, results []models.HeatResult) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var12 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var12 == nil {
+ templ_7745c5c3_Var12 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+
+ // Find current heat
+ var currentHeat models.Heat
+ for _, heat := range heats {
+ if heat.HeatNum == currentHeatNum {
+ currentHeat = heat
+ break
+ }
+ }
+
+ // Find heat result if available
+ var currentResult *models.HeatResult
+ for _, result := range results {
+ if result.HeatNumber == currentHeatNum {
+ currentResult = &result
+ break
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Lane Racer Car # Time Position ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if currentHeat.Lane1ID != nil {
+ templ_7745c5c3_Err = raceManageLaneRow(1, *currentHeat.Lane1ID, racers, currentResult).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if currentHeat.Lane2ID != nil {
+ templ_7745c5c3_Err = raceManageLaneRow(2, *currentHeat.Lane2ID, racers, currentResult).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if currentHeat.Lane3ID != nil {
+ templ_7745c5c3_Err = raceManageLaneRow(3, *currentHeat.Lane3ID, racers, currentResult).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if currentHeat.Lane4ID != nil {
+ templ_7745c5c3_Err = raceManageLaneRow(4, *currentHeat.Lane4ID, racers, currentResult).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Helper template for displaying a lane row in the management view
+func raceManageLaneRow(lane int, racerID int64, racers []models.Racer, result *models.HeatResult) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var13 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var13 == nil {
+ templ_7745c5c3_Var13 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+
+ // Find racer
+ var racer models.Racer
+ for _, r := range racers {
+ if r.ID == racerID {
+ racer = r
+ break
+ }
+ }
+
+ // Get time and position from result if available
+ var time float64
+ var position int
+ var hasResult bool
+
+ if result != nil {
+ hasResult = true
+ switch lane {
+ case 1:
+ time = result.Lane1Time
+ position = result.Lane1Position
+ case 2:
+ time = result.Lane2Time
+ position = result.Lane2Position
+ case 3:
+ time = result.Lane3Time
+ position = result.Lane3Position
+ case 4:
+ time = result.Lane4Time
+ position = result.Lane4Position
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var14 string
+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(lane))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_manage.templ`, Line: 371, Col: 32}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var15 string
+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(racer.FirstName)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_manage.templ`, Line: 372, Col: 29}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var16 string
+ templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(racer.LastName)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_manage.templ`, Line: 372, Col: 48}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var17 string
+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(racer.CarNumber)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_manage.templ`, Line: 373, Col: 29}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if hasResult {
+ var templ_7745c5c3_Var19 string
+ templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.3f", time))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_manage.templ`, Line: 376, Col: 43}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "--.-")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if hasResult {
+ var templ_7745c5c3_Var21 string
+ templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(position))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_manage.templ`, Line: 383, Col: 40}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "-")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Helper template for displaying next heat in the management view
+func raceManageNextHeat(heats []models.Heat, racers []models.Racer, heatNum int) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var22 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var22 == nil {
+ templ_7745c5c3_Var22 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+
+ // Find the heat
+ var nextHeat models.Heat
+ for _, heat := range heats {
+ if heat.HeatNum == heatNum {
+ nextHeat = heat
+ break
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "Lane Racer Car # ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if nextHeat.Lane1ID != nil {
+ templ_7745c5c3_Err = raceManageNextHeatRow(1, *nextHeat.Lane1ID, racers).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if nextHeat.Lane2ID != nil {
+ templ_7745c5c3_Err = raceManageNextHeatRow(2, *nextHeat.Lane2ID, racers).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if nextHeat.Lane3ID != nil {
+ templ_7745c5c3_Err = raceManageNextHeatRow(3, *nextHeat.Lane3ID, racers).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if nextHeat.Lane4ID != nil {
+ templ_7745c5c3_Err = raceManageNextHeatRow(4, *nextHeat.Lane4ID, racers).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Helper template for displaying a row in the next heat preview
+func raceManageNextHeatRow(lane int, racerID int64, racers []models.Racer) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var23 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var23 == nil {
+ templ_7745c5c3_Var23 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+
+ // Find racer
+ var racer models.Racer
+ for _, r := range racers {
+ if r.ID == racerID {
+ racer = r
+ break
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var24 string
+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(lane))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_manage.templ`, Line: 445, Col: 32}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var25 string
+ templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(racer.FirstName)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_manage.templ`, Line: 446, Col: 29}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var26 string
+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(racer.LastName)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_manage.templ`, Line: 446, Col: 48}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var27 string
+ templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(racer.CarNumber)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_manage.templ`, Line: 447, Col: 29}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/web/templates/race_public.templ b/web/templates/race_public.templ
new file mode 100644
index 0000000..a21839f
--- /dev/null
+++ b/web/templates/race_public.templ
@@ -0,0 +1,297 @@
+package templates
+
+import (
+ "track-gopher/models"
+ "fmt"
+ "strconv"
+)
+
+// RacePublic renders the public race view
+templ RacePublic(currentGroup models.Group, heats []models.Heat, racers []models.Racer, currentHeatNum int, results []models.HeatResult) {
+ @Layout("Race - " + currentGroup.Name) {
+
+
+
+
+
+
+
+
+
+
+
+
+ @raceCurrentHeatLanes(heats, racers, currentHeatNum, results)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ if currentHeatNum < len(heats) {
+ @raceNextHeatPreview(heats, racers, currentHeatNum+1)
+ } else {
+
No more heats in this group
+ }
+
+
+
+
+
+
+
+ if currentHeatNum+1 < len(heats) {
+ @raceNextHeatPreview(heats, racers, currentHeatNum+2)
+ } else {
+
No more heats in this group
+ }
+
+
+
+
+
+
+
+ }
+}
+
+// Helper template for displaying current heat lanes
+templ raceCurrentHeatLanes(heats []models.Heat, racers []models.Racer, currentHeatNum int, results []models.HeatResult) {
+ {{
+ // Find current heat
+ var currentHeat models.Heat
+ for _, heat := range heats {
+ if heat.HeatNum == currentHeatNum {
+ currentHeat = heat
+ break
+ }
+ }
+
+ // Find heat result if available
+ var currentResult *models.HeatResult
+ for _, result := range results {
+ if result.HeatNumber == currentHeatNum {
+ currentResult = &result
+ break
+ }
+ }
+ }}
+
+
+ if currentHeat.Lane1ID != nil {
+ @raceLaneCard(1, *currentHeat.Lane1ID, racers, currentResult)
+ }
+ if currentHeat.Lane2ID != nil {
+ @raceLaneCard(2, *currentHeat.Lane2ID, racers, currentResult)
+ }
+ if currentHeat.Lane3ID != nil {
+ @raceLaneCard(3, *currentHeat.Lane3ID, racers, currentResult)
+ }
+ if currentHeat.Lane4ID != nil {
+ @raceLaneCard(4, *currentHeat.Lane4ID, racers, currentResult)
+ }
+
+}
+
+// Helper template for displaying a lane card
+templ raceLaneCard(lane int, racerID int64, racers []models.Racer, result *models.HeatResult) {
+ {{
+ // Find racer
+ var racer models.Racer
+ for _, r := range racers {
+ if r.ID == racerID {
+ racer = r
+ break
+ }
+ }
+
+ // Get time and position from result if available
+ var time float64
+ var position int
+ var hasResult bool
+
+ if result != nil {
+ hasResult = true
+ switch lane {
+ case 1:
+ time = result.Lane1Time
+ position = result.Lane1Position
+ case 2:
+ time = result.Lane2Time
+ position = result.Lane2Position
+ case 3:
+ time = result.Lane3Time
+ position = result.Lane3Position
+ case 4:
+ time = result.Lane4Time
+ position = result.Lane4Position
+ }
+ }
+ }}
+
+
+
+
+
+
{ racer.FirstName } { racer.LastName }
+
+ Car #: { racer.CarNumber }
+ Weight: { fmt.Sprintf("%.1f oz", racer.CarWeight) }
+
+
+
+
+
+
Time
+
+ if hasResult {
+ { fmt.Sprintf("%.3f", time) }
+ } else {
+ --.-
+ }
+
+
+
+
+
+
Position
+
+ if hasResult {
+ { strconv.Itoa(position) }
+ } else {
+ -
+ }
+
+
+
+
+
+
+
+
+}
+
+// Helper template for displaying next heat preview
+templ raceNextHeatPreview(heats []models.Heat, racers []models.Racer, heatNum int) {
+ {{
+ // Find the heat
+ var nextHeat models.Heat
+ for _, heat := range heats {
+ if heat.HeatNum == heatNum {
+ nextHeat = heat
+ break
+ }
+ }
+ }}
+
+ Heat { strconv.Itoa(heatNum) }
+
+
+
+
+ Lane
+ Racer
+ Car #
+
+
+
+ if nextHeat.Lane1ID != nil {
+ @raceNextHeatRow(1, *nextHeat.Lane1ID, racers)
+ }
+ if nextHeat.Lane2ID != nil {
+ @raceNextHeatRow(2, *nextHeat.Lane2ID, racers)
+ }
+ if nextHeat.Lane3ID != nil {
+ @raceNextHeatRow(3, *nextHeat.Lane3ID, racers)
+ }
+ if nextHeat.Lane4ID != nil {
+ @raceNextHeatRow(4, *nextHeat.Lane4ID, racers)
+ }
+
+
+
+}
+
+// Helper template for displaying a row in the next heat preview
+templ raceNextHeatRow(lane int, racerID int64, racers []models.Racer) {
+ {{
+ // Find racer
+ var racer models.Racer
+ for _, r := range racers {
+ if r.ID == racerID {
+ racer = r
+ break
+ }
+ }
+ }}
+
+
+ { strconv.Itoa(lane) }
+ { racer.FirstName } { racer.LastName }
+ { racer.CarNumber }
+
+}
\ No newline at end of file
diff --git a/web/templates/race_public_templ.go b/web/templates/race_public_templ.go
new file mode 100644
index 0000000..b1b230a
--- /dev/null
+++ b/web/templates/race_public_templ.go
@@ -0,0 +1,582 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.3.833
+package templates
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+import (
+ "fmt"
+ "strconv"
+ "track-gopher/models"
+)
+
+// RacePublic renders the public race view
+func RacePublic(currentGroup models.Group, heats []models.Heat, racers []models.Racer, currentHeatNum int, results []models.HeatResult) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = raceCurrentHeatLanes(heats, racers, currentHeatNum, results).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if currentHeatNum < len(heats) {
+ templ_7745c5c3_Err = raceNextHeatPreview(heats, racers, currentHeatNum+1).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
No more heats in this group
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if currentHeatNum+1 < len(heats) {
+ templ_7745c5c3_Err = raceNextHeatPreview(heats, racers, currentHeatNum+2).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
No more heats in this group
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = Layout("Race - "+currentGroup.Name).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Helper template for displaying current heat lanes
+func raceCurrentHeatLanes(heats []models.Heat, racers []models.Racer, currentHeatNum int, results []models.HeatResult) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var6 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var6 == nil {
+ templ_7745c5c3_Var6 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+
+ // Find current heat
+ var currentHeat models.Heat
+ for _, heat := range heats {
+ if heat.HeatNum == currentHeatNum {
+ currentHeat = heat
+ break
+ }
+ }
+
+ // Find heat result if available
+ var currentResult *models.HeatResult
+ for _, result := range results {
+ if result.HeatNumber == currentHeatNum {
+ currentResult = &result
+ break
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if currentHeat.Lane1ID != nil {
+ templ_7745c5c3_Err = raceLaneCard(1, *currentHeat.Lane1ID, racers, currentResult).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if currentHeat.Lane2ID != nil {
+ templ_7745c5c3_Err = raceLaneCard(2, *currentHeat.Lane2ID, racers, currentResult).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if currentHeat.Lane3ID != nil {
+ templ_7745c5c3_Err = raceLaneCard(3, *currentHeat.Lane3ID, racers, currentResult).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if currentHeat.Lane4ID != nil {
+ templ_7745c5c3_Err = raceLaneCard(4, *currentHeat.Lane4ID, racers, currentResult).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Helper template for displaying a lane card
+func raceLaneCard(lane int, racerID int64, racers []models.Racer, result *models.HeatResult) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var7 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var7 == nil {
+ templ_7745c5c3_Var7 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+
+ // Find racer
+ var racer models.Racer
+ for _, r := range racers {
+ if r.ID == racerID {
+ racer = r
+ break
+ }
+ }
+
+ // Get time and position from result if available
+ var time float64
+ var position int
+ var hasResult bool
+
+ if result != nil {
+ hasResult = true
+ switch lane {
+ case 1:
+ time = result.Lane1Time
+ position = result.Lane1Position
+ case 2:
+ time = result.Lane2Time
+ position = result.Lane2Position
+ case 3:
+ time = result.Lane3Time
+ position = result.Lane3Position
+ case 4:
+ time = result.Lane4Time
+ position = result.Lane4Position
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var9 string
+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(racer.FirstName)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_public.templ`, Line: 200, Col: 56}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var10 string
+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(racer.LastName)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_public.templ`, Line: 200, Col: 75}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " Car #: ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var11 string
+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(racer.CarNumber)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_public.templ`, Line: 202, Col: 61}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "Weight: ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var12 string
+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f oz", racer.CarWeight))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_public.templ`, Line: 203, Col: 86}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
Time ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if hasResult {
+ var templ_7745c5c3_Var14 string
+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.3f", time))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_public.templ`, Line: 212, Col: 67}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "--.-")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
Position ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if hasResult {
+ var templ_7745c5c3_Var16 string
+ templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(position))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_public.templ`, Line: 224, Col: 64}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "-")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Helper template for displaying next heat preview
+func raceNextHeatPreview(heats []models.Heat, racers []models.Racer, heatNum int) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var17 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var17 == nil {
+ templ_7745c5c3_Var17 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+
+ // Find the heat
+ var nextHeat models.Heat
+ for _, heat := range heats {
+ if heat.HeatNum == heatNum {
+ nextHeat = heat
+ break
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Heat ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var18 string
+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(heatNum))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_public.templ`, Line: 251, Col: 49}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " Lane Racer Car # ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if nextHeat.Lane1ID != nil {
+ templ_7745c5c3_Err = raceNextHeatRow(1, *nextHeat.Lane1ID, racers).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if nextHeat.Lane2ID != nil {
+ templ_7745c5c3_Err = raceNextHeatRow(2, *nextHeat.Lane2ID, racers).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if nextHeat.Lane3ID != nil {
+ templ_7745c5c3_Err = raceNextHeatRow(3, *nextHeat.Lane3ID, racers).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if nextHeat.Lane4ID != nil {
+ templ_7745c5c3_Err = raceNextHeatRow(4, *nextHeat.Lane4ID, racers).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Helper template for displaying a row in the next heat preview
+func raceNextHeatRow(lane int, racerID int64, racers []models.Racer) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var19 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var19 == nil {
+ templ_7745c5c3_Var19 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+
+ // Find racer
+ var racer models.Racer
+ for _, r := range racers {
+ if r.ID == racerID {
+ racer = r
+ break
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var20 string
+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(lane))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_public.templ`, Line: 293, Col: 32}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var21 string
+ templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(racer.FirstName)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_public.templ`, Line: 294, Col: 29}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var22 string
+ templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(racer.LastName)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_public.templ`, Line: 294, Col: 48}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var23 string
+ templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(racer.CarNumber)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/race_public.templ`, Line: 295, Col: 29}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate