diff --git a/db/schema.go b/db/schema.go index 5d58c62..473f333 100644 --- a/db/schema.go +++ b/db/schema.go @@ -16,7 +16,7 @@ type DB struct { logger *slog.Logger } -// New creates a new database connection +// NewDB creates a new database connection func New(dbPath string) (*DB, error) { logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelDebug, @@ -112,5 +112,170 @@ func (db *DB) initSchema() error { return fmt.Errorf("failed to create race_results table: %w", err) } + // Create heats table + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS heats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + group_id INTEGER NOT NULL, + heat_num INTEGER NOT NULL, + lane1_id INTEGER, + lane2_id INTEGER, + lane3_id INTEGER, + lane4_id INTEGER, + FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE, + FOREIGN KEY (lane1_id) REFERENCES racers(id) ON DELETE SET NULL, + FOREIGN KEY (lane2_id) REFERENCES racers(id) ON DELETE SET NULL, + FOREIGN KEY (lane3_id) REFERENCES racers(id) ON DELETE SET NULL, + FOREIGN KEY (lane4_id) REFERENCES racers(id) ON DELETE SET NULL + ) + `) + if err != nil { + return fmt.Errorf("failed to create heats table: %w", err) + } + return nil } + +// Close closes the database connection +func (db *DB) Close() error { + return db.DB.Close() +} + +// Group represents a group of racers +type Group struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` +} + +// Racer represents a racer in the derby +type Racer struct { + ID int64 `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + CarNumber string `json:"car_number"` + CarWeight float64 `json:"car_weight"` + GroupID int64 `json:"group_id"` +} + +// Heat represents a single race with 4 lanes +type Heat struct { + ID int64 `json:"id"` + GroupID int64 `json:"group_id"` + HeatNum int `json:"heat_num"` + Lane1ID *int64 `json:"lane1_id"` + Lane2ID *int64 `json:"lane2_id"` + Lane3ID *int64 `json:"lane3_id"` + Lane4ID *int64 `json:"lane4_id"` +} + +// SaveHeats saves a list of heats for a group +func (db *DB) SaveHeats(groupID int64, heats []Heat) error { + // Start a transaction + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + // Delete existing heats for this group + _, err = tx.Exec("DELETE FROM heats WHERE group_id = ?", groupID) + if err != nil { + return err + } + + // Insert new heats + stmt, err := tx.Prepare(` + INSERT INTO heats (group_id, heat_num, lane1_id, lane2_id, lane3_id, lane4_id) + VALUES (?, ?, ?, ?, ?, ?) + `) + if err != nil { + return err + } + defer stmt.Close() + + for _, heat := range heats { + _, err = stmt.Exec( + groupID, + heat.HeatNum, + nullableInt64(heat.Lane1ID), + nullableInt64(heat.Lane2ID), + nullableInt64(heat.Lane3ID), + nullableInt64(heat.Lane4ID), + ) + if err != nil { + return err + } + } + + // Commit the transaction + return tx.Commit() +} + +// Helper function to handle nullable int64 values +func nullableInt64(i *int64) interface{} { + if i == nil { + return nil + } + return *i +} + +// GetHeats retrieves all heats for a group +func (db *DB) GetHeats(groupID int64) ([]Heat, error) { + rows, err := db.Query(` + SELECT id, group_id, heat_num, lane1_id, lane2_id, lane3_id, lane4_id + FROM heats + WHERE group_id = ? + ORDER BY heat_num + `, groupID) + if err != nil { + return nil, err + } + defer rows.Close() + + var heats []Heat + for rows.Next() { + var heat Heat + var lane1, lane2, lane3, lane4 sql.NullInt64 + + err := rows.Scan( + &heat.ID, + &heat.GroupID, + &heat.HeatNum, + &lane1, + &lane2, + &lane3, + &lane4, + ) + if err != nil { + return nil, err + } + + if lane1.Valid { + val := lane1.Int64 + heat.Lane1ID = &val + } + if lane2.Valid { + val := lane2.Int64 + heat.Lane2ID = &val + } + if lane3.Valid { + val := lane3.Int64 + heat.Lane3ID = &val + } + if lane4.Valid { + val := lane4.Int64 + heat.Lane4ID = &val + } + + heats = append(heats, heat) + } + + return heats, nil +} + +// DeleteHeats deletes all heats for a group +func (db *DB) DeleteHeats(groupID int64) error { + _, err := db.Exec("DELETE FROM heats WHERE group_id = ?", groupID) + return err +} diff --git a/derby/heats.go b/derby/heats.go new file mode 100644 index 0000000..944c247 --- /dev/null +++ b/derby/heats.go @@ -0,0 +1,181 @@ +package derby + +import ( + "math/rand" + "time" +) + +// Racer represents a racer for heat generation +type Racer struct { + ID int64 `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + CarNumber string `json:"car_number"` +} + +// Heat represents a single race with 4 lanes +type Heat struct { + HeatNum int `json:"heat_num"` + Lane1ID *int64 `json:"lane1_id"` + Lane2ID *int64 `json:"lane2_id"` + Lane3ID *int64 `json:"lane3_id"` + Lane4ID *int64 `json:"lane4_id"` +} + +func CreateRacer(id int64, firstName string, lastName string, carNumber string) Racer { + return Racer{ID: id, FirstName: firstName, LastName: lastName, CarNumber: carNumber} +} + +// GenerateHeats creates a set of heats for a group of racers +// Each racer should race in each lane once, with different opponents when possible +func GenerateHeats(racers []Racer) []Heat { + // If no racers, return empty heats + if len(racers) == 0 { + return []Heat{} + } + + // Seed the random number generator + rand.Seed(time.Now().UnixNano()) + + // Shuffle racers to randomize initial order + shuffledRacers := make([]Racer, len(racers)) + copy(shuffledRacers, racers) + rand.Shuffle(len(shuffledRacers), func(i, j int) { + shuffledRacers[i], shuffledRacers[j] = shuffledRacers[j], shuffledRacers[i] + }) + + // Track which lanes each racer has used + racerLanes := make(map[int64]map[int]bool) + for _, racer := range racers { + racerLanes[racer.ID] = make(map[int]bool) + } + + // Track which racers have raced against each other + racerOpponents := make(map[int64]map[int64]bool) + for _, racer := range racers { + racerOpponents[racer.ID] = make(map[int64]bool) + } + + // Calculate number of heats needed + // Each racer needs to race in 4 lanes, so total races = (racers * 4) / 4 = racers + // But we need to round up to ensure all racers get all lanes + numHeats := len(racers) + if len(racers) < 4 { + numHeats = 1 + } else if len(racers)%4 != 0 { + // Add extra heats to ensure all racers get all lanes + numHeats = (len(racers) + 3) / 4 * 4 + } + + // Generate heats + heats := make([]Heat, 0, numHeats) + heatNum := 1 + + // Create a queue of racers that need to race in each lane + laneQueues := make([][]int64, 4) + for i := 0; i < 4; i++ { + laneQueues[i] = make([]int64, 0, len(racers)) + for _, racer := range shuffledRacers { + laneQueues[i] = append(laneQueues[i], racer.ID) + } + // Shuffle each lane queue differently + rand.Shuffle(len(laneQueues[i]), func(j, k int) { + laneQueues[i][j], laneQueues[i][k] = laneQueues[i][k], laneQueues[i][j] + }) + } + + // Keep generating heats until all racers have raced in all lanes + for { + // Check if all racers have raced in all lanes + allDone := true + for _, racer := range racers { + if len(racerLanes[racer.ID]) < 4 { + allDone = false + break + } + } + if allDone { + break + } + + // Create a new heat + heat := Heat{ + HeatNum: heatNum, + } + heatNum++ + + // Fill lanes + filledLanes := 0 + usedRacers := make(map[int64]bool) + + // Try to fill each lane + for lane := 1; lane <= 4; lane++ { + // Find a racer for this lane + var selectedRacer *int64 = nil + + // First try racers who haven't used this lane yet + for i := 0; i < len(laneQueues[lane-1]); i++ { + racerID := laneQueues[lane-1][i] + + // Skip if racer already in this heat + if usedRacers[racerID] { + continue + } + + // Skip if racer already used this lane + if racerLanes[racerID][lane] { + continue + } + + // Use this racer + selectedRacer = &racerID + + // Remove from queue + laneQueues[lane-1] = append(laneQueues[lane-1][:i], laneQueues[lane-1][i+1:]...) + + break + } + + // If no racer found, leave lane empty + if selectedRacer != nil { + // Mark lane as used for this racer + racerLanes[*selectedRacer][lane] = true + + // Mark racer as used in this heat + usedRacers[*selectedRacer] = true + + // Update opponents + for otherRacerID := range usedRacers { + if otherRacerID != *selectedRacer { + racerOpponents[*selectedRacer][otherRacerID] = true + racerOpponents[otherRacerID][*selectedRacer] = true + } + } + + filledLanes++ + } + + // Set lane in heat + switch lane { + case 1: + heat.Lane1ID = selectedRacer + case 2: + heat.Lane2ID = selectedRacer + case 3: + heat.Lane3ID = selectedRacer + case 4: + heat.Lane4ID = selectedRacer + } + } + + // Only add heat if at least one lane is filled + if filledLanes > 0 { + heats = append(heats, heat) + } else { + // If we couldn't fill any lanes, we're done + break + } + } + + return heats +} diff --git a/web/server.go b/web/server.go index e923ecf..48a2eb9 100644 --- a/web/server.go +++ b/web/server.go @@ -129,6 +129,15 @@ func (s *Server) routes() { r.Delete("/{id}", s.handleDeleteRacer()) }) + // Add heats page route + s.router.Get("/heats", s.handleHeats()) + + // Add heats API routes + s.router.Route("/api/heats", func(r chi.Router) { + r.Post("/generate", s.handleGenerateHeats()) + r.Post("/save", s.handleSaveHeats()) + }) + // Main page s.router.Get("/", s.handleIndex()) } @@ -656,3 +665,155 @@ func (s *Server) handleDeleteRacer() http.HandlerFunc { fmt.Fprintf(w, `{"success":true}`) } } + +// handleHeats renders the heats page +func (s *Server) handleHeats() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Get groups from database + 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 racers from database + racers, err := s.db.GetRacers() + if err != nil { + s.logger.Error("Failed to get racers", "error", err) + http.Error(w, "Failed to get racers", http.StatusInternalServerError) + return + } + + // Get selected group ID from query parameter + selectedGroupID := int64(0) + groupIDStr := r.URL.Query().Get("group_id") + if groupIDStr != "" { + groupID, err := strconv.ParseInt(groupIDStr, 10, 64) + if err == nil { + selectedGroupID = groupID + } + } + + // Render template + component := templates.Heats(groups, racers, selectedGroupID) + if err := component.Render(r.Context(), w); err != nil { + s.logger.Error("Failed to render heats template", "error", err) + http.Error(w, "Failed to render page", http.StatusInternalServerError) + } + } +} + +// handleGenerateHeats generates heats for a group +func (s *Server) handleGenerateHeats() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Get group ID from query parameter + groupIDStr := r.URL.Query().Get("group_id") + groupID, err := strconv.ParseInt(groupIDStr, 10, 64) + if err != nil { + http.Error(w, "Invalid group ID", http.StatusBadRequest) + return + } + + // Get racers for this group + allRacers, err := s.db.GetRacers() + if err != nil { + s.logger.Error("Failed to get racers", "error", err) + http.Error(w, "Failed to get racers", http.StatusInternalServerError) + return + } + + // Filter racers by group + var groupRacers []db.Racer + for _, racer := range allRacers { + if racer.GroupID == groupID { + groupRacers = append(groupRacers, racer) + } + } + + // Convert to derby racers + derbyRacers := make([]derby.Racer, len(groupRacers)) + for i, racer := range groupRacers { + derbyRacers[i] = derby.Racer{ + ID: racer.ID, + FirstName: racer.FirstName, + LastName: racer.LastName, + CarNumber: racer.CarNumber, + } + } + + // Render the generated heats + component := templates.GeneratedHeats(groupRacers) + if err := component.Render(r.Context(), w); err != nil { + s.logger.Error("Failed to render heats", "error", err) + http.Error(w, "Failed to render heats", http.StatusInternalServerError) + } + } +} + +// handleSaveHeats saves heats for a group +func (s *Server) handleSaveHeats() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Get group ID from query parameter + groupIDStr := r.URL.Query().Get("group_id") + groupID, err := strconv.ParseInt(groupIDStr, 10, 64) + if err != nil { + http.Error(w, "Invalid group ID", http.StatusBadRequest) + return + } + + // Get racers for this group + allRacers, err := s.db.GetRacers() + if err != nil { + s.logger.Error("Failed to get racers", "error", err) + http.Error(w, "Failed to get racers", http.StatusInternalServerError) + return + } + + // Filter racers by group + var groupRacers []db.Racer + for _, racer := range allRacers { + if racer.GroupID == groupID { + groupRacers = append(groupRacers, racer) + } + } + + // Convert to derby racers + derbyRacers := make([]derby.Racer, len(groupRacers)) + for i, racer := range groupRacers { + derbyRacers[i] = derby.Racer{ + ID: racer.ID, + FirstName: racer.FirstName, + LastName: racer.LastName, + CarNumber: racer.CarNumber, + } + } + + // Generate heats + derbyHeats := derby.GenerateHeats(derbyRacers) + + // Convert to database heats + dbHeats := make([]db.Heat, len(derbyHeats)) + for i, heat := range derbyHeats { + dbHeats[i] = db.Heat{ + GroupID: groupID, + HeatNum: heat.HeatNum, + Lane1ID: heat.Lane1ID, + Lane2ID: heat.Lane2ID, + Lane3ID: heat.Lane3ID, + Lane4ID: heat.Lane4ID, + } + } + + // Save heats to database + if err := s.db.SaveHeats(groupID, dbHeats); err != nil { + s.logger.Error("Failed to save heats", "error", err) + http.Error(w, "Failed to save heats", http.StatusInternalServerError) + return + } + + // Return success message + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(`

Heats saved successfully!

`)) + } +} diff --git a/web/templates/heats.templ b/web/templates/heats.templ new file mode 100644 index 0000000..1d55098 --- /dev/null +++ b/web/templates/heats.templ @@ -0,0 +1,182 @@ +package templates + +import ( + "strconv" + "track-gopher/db" + "track-gopher/derby" +) + +templ Heats(groups []db.Group, racers []db.Racer, selectedGroupID int64) { + @Layout("Race Heats") { +
+

Race Heats Generator

+ +
+ + +
+ +
+ if selectedGroupID > 0 { + @HeatsContent(selectedGroupID, groups, racers) + } else { +
+

Please select a group to generate heats.

+
+ } +
+
+ } +} + +templ HeatsContent(groupID int64, groups []db.Group, allRacers []db.Racer) { + // Filter racers by group + var groupRacers []db.Racer + for _, racer := range allRacers { + if racer.GroupID == groupID { + groupRacers = append(groupRacers, racer) + } + } + +
+
+

+ Heats for { getGroupName(groups, groupID) } ({ strconv.Itoa(len(groupRacers)) } racers) +

+
+ + +
+
+ +
+ + if len(groupRacers) == 0 { +
+

No racers in this group. Add racers to generate heats.

+
+ } else { +
+ @GeneratedHeats(groupRacers) +
+ } +
+} + +templ GeneratedHeats(racers []db.Racer) { + // Convert db.Racer to derby.Racer for heat generation + var derbyRacers []derby.Racer + for _, racer := range racers { + derbyRacers = append(derbyRacers, derby.CreateRacer(racer.ID, racer.FirstName, racer.LastName, racer.CarNumber)) + } + + var heats = derby.GenerateHeats(derbyRacers) + + if len(heats) == 0 { +
+

No heats could be generated. Please add more racers.

+
+ } else { +
+ for _, heat := range heats { +
+

Heat { strconv.Itoa(heat.HeatNum) }

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LaneRacerCar #
1{ getRacerName(racers, heat.Lane1ID) }{ getRacerCarNumber(racers, heat.Lane1ID) }
2{ getRacerName(racers, heat.Lane2ID) }{ getRacerCarNumber(racers, heat.Lane2ID) }
3{ getRacerName(racers, heat.Lane3ID) }{ getRacerCarNumber(racers, heat.Lane3ID) }
4{ getRacerName(racers, heat.Lane4ID) }{ getRacerCarNumber(racers, heat.Lane4ID) }
+
+ } +
+ } +} + +func getGroupName(groups []db.Group, groupID int64) string { + for _, group := range groups { + if group.ID == groupID { + return group.Name + } + } + return "Unknown Group" +} + +func getRacerName(racers []db.Racer, racerID *int64) string { + if racerID == nil { + return "Empty" + } + + for _, racer := range racers { + if racer.ID == *racerID { + return racer.FirstName + " " + racer.LastName + } + } + return "Unknown Racer" +} + +func getRacerCarNumber(racers []db.Racer, racerID *int64) string { + if racerID == nil { + return "-" + } + + for _, racer := range racers { + if racer.ID == *racerID { + return racer.CarNumber + } + } + return "-" +} diff --git a/web/templates/heats_templ.go b/web/templates/heats_templ.go new file mode 100644 index 0000000..5287625 --- /dev/null +++ b/web/templates/heats_templ.go @@ -0,0 +1,453 @@ +// 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 ( + "strconv" + "track-gopher/db" + "track-gopher/derby" +) + +func Heats(groups []db.Group, racers []db.Racer, selectedGroupID int64) 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, "

Race Heats Generator

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if selectedGroupID > 0 { + templ_7745c5c3_Err = HeatsContent(selectedGroupID, groups, racers).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

Please select a group to generate heats.

") + 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 Heats").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func HeatsContent(groupID int64, groups []db.Group, allRacers []db.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_Var5 := templ.GetChildren(ctx) + if templ_7745c5c3_Var5 == nil { + templ_7745c5c3_Var5 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "var groupRacers []db.Racer ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, racer := range allRacers { + if racer.GroupID == groupID { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "groupRacers = append(groupRacers, racer)") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

Heats for ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(getGroupName(groups, groupID)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/heats.templ`, Line: 59, Col: 45} + } + _, 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, 13, " (") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(len(groupRacers))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/heats.templ`, Line: 59, Col: 81} + } + _, 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, 14, " racers)

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(groupRacers) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

No racers in this group. Add racers to generate heats.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = GeneratedHeats(groupRacers).Render(ctx, templ_7745c5c3_Buffer) + 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 + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func GeneratedHeats(racers []db.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_Var10 := templ.GetChildren(ctx) + if templ_7745c5c3_Var10 == nil { + templ_7745c5c3_Var10 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "var derbyRacers []derby.Racer ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, racer := range racers { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "derbyRacers = append(derbyRacers, derby.CreateRacer(racer.ID, racer.FirstName, racer.LastName, racer.CarNumber)) ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "var heats = derby.GenerateHeats(derbyRacers) ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(heats) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "

No heats could be generated. Please add more racers.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, heat := range heats { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "

Heat ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(heat.HeatNum)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/heats.templ`, Line: 111, Col: 77} + } + _, 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, 27, "

LaneRacerCar #
1") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(getRacerName(racers, heat.Lane1ID)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/heats.templ`, Line: 123, Col: 66} + } + _, 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, 28, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(getRacerCarNumber(racers, heat.Lane1ID)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/heats.templ`, Line: 124, Col: 71} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
2") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(getRacerName(racers, heat.Lane2ID)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/heats.templ`, Line: 128, Col: 66} + } + _, 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, 30, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(getRacerCarNumber(racers, heat.Lane2ID)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/heats.templ`, Line: 129, Col: 71} + } + _, 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, 31, "
3") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(getRacerName(racers, heat.Lane3ID)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/heats.templ`, Line: 133, Col: 66} + } + _, 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, 32, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(getRacerCarNumber(racers, heat.Lane3ID)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/heats.templ`, Line: 134, Col: 71} + } + _, 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, 33, "
4") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(getRacerName(racers, heat.Lane4ID)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/heats.templ`, Line: 138, Col: 66} + } + _, 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, 34, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(getRacerCarNumber(racers, heat.Lane4ID)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/heats.templ`, Line: 139, Col: 71} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + 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 + }) +} + +func getGroupName(groups []db.Group, groupID int64) string { + for _, group := range groups { + if group.ID == groupID { + return group.Name + } + } + return "Unknown Group" +} + +func getRacerName(racers []db.Racer, racerID *int64) string { + if racerID == nil { + return "Empty" + } + + for _, racer := range racers { + if racer.ID == *racerID { + return racer.FirstName + " " + racer.LastName + } + } + return "Unknown Racer" +} + +func getRacerCarNumber(racers []db.Racer, racerID *int64) string { + if racerID == nil { + return "-" + } + + for _, racer := range racers { + if racer.ID == *racerID { + return racer.CarNumber + } + } + return "-" +} + +var _ = templruntime.GeneratedTemplate diff --git a/web/templates/layout.templ b/web/templates/layout.templ new file mode 100644 index 0000000..2466a8d --- /dev/null +++ b/web/templates/layout.templ @@ -0,0 +1,30 @@ +package templates + +templ Layout(title string) { + + + + + + { title } - Derby Race Manager + + + + + +
+ { children... } +
+ + +} \ No newline at end of file diff --git a/web/templates/layout_templ.go b/web/templates/layout_templ.go new file mode 100644 index 0000000..e2da3fb --- /dev/null +++ b/web/templates/layout_templ.go @@ -0,0 +1,61 @@ +// 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" + +func Layout(title string) 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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/layout.templ`, Line: 9, Col: 17} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Derby Race Manager
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate