diff --git a/db/operations.go b/db/operations.go new file mode 100644 index 0000000..59cd50b --- /dev/null +++ b/db/operations.go @@ -0,0 +1,232 @@ +package db + +import ( + "database/sql" + "fmt" + "time" + + "track-gopher/models" +) + +// Group operations + +// CreateGroup creates a new group +func (db *DB) CreateGroup(name, description string) (int64, error) { + result, err := db.Exec( + "INSERT INTO groups (name, description) VALUES (?, ?)", + name, description, + ) + if err != nil { + return 0, fmt.Errorf("failed to create group: %w", err) + } + + id, err := result.LastInsertId() + if err != nil { + return 0, fmt.Errorf("failed to get last insert ID: %w", err) + } + + return id, nil +} + +// GetGroups returns all groups +func (db *DB) GetGroups() ([]models.Group, error) { + rows, err := db.Query("SELECT id, name, description, created_at FROM groups ORDER BY name") + if err != nil { + return nil, fmt.Errorf("failed to query groups: %w", err) + } + defer rows.Close() + + var groups []models.Group + for rows.Next() { + var g models.Group + var createdAt string + if err := rows.Scan(&g.ID, &g.Name, &g.Description, &createdAt); err != nil { + return nil, fmt.Errorf("failed to scan group row: %w", err) + } + g.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt) + groups = append(groups, g) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating group rows: %w", err) + } + + return groups, nil +} + +// GetGroup returns a group by ID +func (db *DB) GetGroup(id int64) (*models.Group, error) { + var g models.Group + var createdAt string + err := db.QueryRow( + "SELECT id, name, description, created_at FROM groups WHERE id = ?", + id, + ).Scan(&g.ID, &g.Name, &g.Description, &createdAt) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, fmt.Errorf("failed to query group: %w", err) + } + + g.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt) + return &g, nil +} + +// UpdateGroup updates a group +func (db *DB) UpdateGroup(id int64, name, description string) error { + _, err := db.Exec( + "UPDATE groups SET name = ?, description = ? WHERE id = ?", + name, description, id, + ) + if err != nil { + return fmt.Errorf("failed to update group: %w", err) + } + return nil +} + +// DeleteGroup deletes a group +func (db *DB) DeleteGroup(id int64) error { + _, err := db.Exec("DELETE FROM groups WHERE id = ?", id) + if err != nil { + return fmt.Errorf("failed to delete group: %w", err) + } + return nil +} + +// Racer operations + +// CreateRacer creates a new racer +func (db *DB) CreateRacer(firstName, lastName, carNumber string, carWeight float64, groupID int64) (int64, error) { + result, err := db.Exec( + "INSERT INTO racers (first_name, last_name, car_number, car_weight, group_id) VALUES (?, ?, ?, ?, ?)", + firstName, lastName, carNumber, carWeight, groupID, + ) + if err != nil { + return 0, fmt.Errorf("failed to create racer: %w", err) + } + + id, err := result.LastInsertId() + if err != nil { + return 0, fmt.Errorf("failed to get last insert ID: %w", err) + } + + return id, nil +} + +// GetRacers returns all racers +func (db *DB) GetRacers() ([]models.Racer, error) { + rows, err := db.Query(` + SELECT r.id, r.first_name, r.last_name, r.car_number, r.car_weight, + r.group_id, g.name as group_name, r.created_at + FROM racers r + JOIN groups g ON r.group_id = g.id + ORDER BY r.last_name, r.first_name + `) + if err != nil { + return nil, fmt.Errorf("failed to query racers: %w", err) + } + defer rows.Close() + + var racers []models.Racer + for rows.Next() { + var r models.Racer + var createdAt string + if err := rows.Scan( + &r.ID, &r.FirstName, &r.LastName, &r.CarNumber, &r.CarWeight, + &r.GroupID, &r.GroupName, &createdAt, + ); err != nil { + return nil, fmt.Errorf("failed to scan racer row: %w", err) + } + r.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt) + racers = append(racers, r) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating racer rows: %w", err) + } + + return racers, nil +} + +// GetRacersByGroup returns racers in a specific group +func (db *DB) GetRacersByGroup(groupID int64) ([]models.Racer, error) { + rows, err := db.Query(` + SELECT r.id, r.first_name, r.last_name, r.car_number, r.car_weight, + r.group_id, g.name as group_name, r.created_at + FROM racers r + JOIN groups g ON r.group_id = g.id + WHERE r.group_id = ? + ORDER BY r.last_name, r.first_name + `, groupID) + if err != nil { + return nil, fmt.Errorf("failed to query racers by group: %w", err) + } + defer rows.Close() + + var racers []models.Racer + for rows.Next() { + var r models.Racer + var createdAt string + if err := rows.Scan( + &r.ID, &r.FirstName, &r.LastName, &r.CarNumber, &r.CarWeight, + &r.GroupID, &r.GroupName, &createdAt, + ); err != nil { + return nil, fmt.Errorf("failed to scan racer row: %w", err) + } + r.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt) + racers = append(racers, r) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating racer rows: %w", err) + } + + return racers, nil +} + +// GetRacer returns a racer by ID +func (db *DB) GetRacer(id int64) (*models.Racer, error) { + var r models.Racer + var createdAt string + err := db.QueryRow(` + SELECT r.id, r.first_name, r.last_name, r.car_number, r.car_weight, + r.group_id, g.name as group_name, r.created_at + FROM racers r + JOIN groups g ON r.group_id = g.id + WHERE r.id = ? + `, id).Scan( + &r.ID, &r.FirstName, &r.LastName, &r.CarNumber, &r.CarWeight, + &r.GroupID, &r.GroupName, &createdAt, + ) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, fmt.Errorf("failed to query racer: %w", err) + } + + r.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt) + return &r, nil +} + +// UpdateRacer updates a racer +func (db *DB) UpdateRacer(id int64, firstName, lastName, carNumber string, carWeight float64, groupID int64) error { + _, err := db.Exec( + "UPDATE racers SET first_name = ?, last_name = ?, car_number = ?, car_weight = ?, group_id = ? WHERE id = ?", + firstName, lastName, carNumber, carWeight, groupID, id, + ) + if err != nil { + return fmt.Errorf("failed to update racer: %w", err) + } + return nil +} + +// DeleteRacer deletes a racer +func (db *DB) DeleteRacer(id int64) error { + _, err := db.Exec("DELETE FROM racers WHERE id = ?", id) + if err != nil { + return fmt.Errorf("failed to delete racer: %w", err) + } + return nil +} diff --git a/db/schema.go b/db/schema.go new file mode 100644 index 0000000..5d58c62 --- /dev/null +++ b/db/schema.go @@ -0,0 +1,116 @@ +package db + +import ( + "database/sql" + "fmt" + "log/slog" + "os" + "path/filepath" + + _ "github.com/mattn/go-sqlite3" +) + +// DB represents the database connection +type DB struct { + *sql.DB + logger *slog.Logger +} + +// New creates a new database connection +func New(dbPath string) (*DB, error) { + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, + })) + + // Ensure directory exists + if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil { + return nil, fmt.Errorf("failed to create database directory: %w", err) + } + + // Open database connection + sqlDB, err := sql.Open("sqlite3", dbPath) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + // Create database instance + db := &DB{ + DB: sqlDB, + logger: logger, + } + + // Initialize schema + if err := db.initSchema(); err != nil { + db.Close() + return nil, fmt.Errorf("failed to initialize schema: %w", err) + } + + return db, nil +} + +// initSchema creates the database tables if they don't exist +func (db *DB) initSchema() error { + // Create groups table + _, err := db.Exec(` + CREATE TABLE IF NOT EXISTS groups ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `) + if err != nil { + return fmt.Errorf("failed to create groups table: %w", err) + } + + // Create racers table + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS racers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + car_number TEXT NOT NULL UNIQUE, + car_weight REAL NOT NULL, + group_id INTEGER NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (group_id) REFERENCES groups(id) + ) + `) + if err != nil { + return fmt.Errorf("failed to create racers table: %w", err) + } + + // Create races table + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS races ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `) + if err != nil { + return fmt.Errorf("failed to create races table: %w", err) + } + + // Create race_results table + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS race_results ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + race_id INTEGER NOT NULL, + racer_id INTEGER NOT NULL, + lane INTEGER NOT NULL, + time REAL, + place INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (race_id) REFERENCES races(id), + FOREIGN KEY (racer_id) REFERENCES racers(id), + UNIQUE(race_id, lane) + ) + `) + if err != nil { + return fmt.Errorf("failed to create race_results table: %w", err) + } + + return nil +} diff --git a/examples/main.go b/examples/main.go index 4bd9759..7339aa6 100644 --- a/examples/main.go +++ b/examples/main.go @@ -30,6 +30,7 @@ func main() { portName := flag.String("port", "/dev/ttyACM0", "Serial port for the derby clock") baudRate := flag.Int("baud", 19200, "Baud rate for the serial port") webPort := flag.Int("web-port", 8080, "Port for the web server") + dbPath := flag.String("db", "./data/derby.db", "Path to SQLite database file") noWeb := flag.Bool("no-web", false, "Disable web interface") noTerminal := flag.Bool("no-terminal", false, "Disable terminal interface") flag.Parse() @@ -72,7 +73,7 @@ func main() { wg.Add(1) go func() { defer wg.Done() - startWebInterface(clock, webEvents, *webPort, ctx) + startWebInterface(clock, webEvents, *webPort, *dbPath, ctx) }() } @@ -100,13 +101,13 @@ func main() { } // startWebInterface initializes and runs the web interface -func startWebInterface(clock *derby.DerbyClock, events <-chan derby.Event, webPort int, ctx context.Context) { +func startWebInterface(clock *derby.DerbyClock, events <-chan derby.Event, webPort int, dbPath string, ctx context.Context) { logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelInfo, })) // Create and start the web server - server, err := web.NewServer(clock, events, webPort) + server, err := web.NewServer(clock, events, dbPath, webPort, logger) if err != nil { logger.Error("Error creating web server", "error", err) return diff --git a/go.mod b/go.mod index bfd7140..1a98bf4 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24 require ( github.com/a-h/templ v0.3.833 github.com/go-chi/chi/v5 v5.2.1 + github.com/mattn/go-sqlite3 v1.14.24 go.bug.st/serial v1.6.2 ) diff --git a/go.sum b/go.sum index 2c9cfef..2270d1e 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= diff --git a/models/models.go b/models/models.go new file mode 100644 index 0000000..07a561e --- /dev/null +++ b/models/models.go @@ -0,0 +1,44 @@ +package models + +import ( + "time" +) + +// Group represents a racer group (e.g., age group, division) +type Group struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + CreatedAt time.Time `json:"created_at"` +} + +// Racer represents a derby racer +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"` + GroupName string `json:"group_name,omitempty"` // For display purposes + CreatedAt time.Time `json:"created_at"` +} + +// Race represents a derby race event +type Race struct { + ID int64 `json:"id"` + Name string `json:"name"` + Status string `json:"status"` // pending, running, completed + CreatedAt time.Time `json:"created_at"` +} + +// RaceResult represents the result of a racer in a race +type RaceResult struct { + ID int64 `json:"id"` + RaceID int64 `json:"race_id"` + RacerID int64 `json:"racer_id"` + Lane int `json:"lane"` + Time *float64 `json:"time,omitempty"` + Place *int `json:"place,omitempty"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/web/server.go b/web/server.go index decd680..e923ecf 100644 --- a/web/server.go +++ b/web/server.go @@ -10,7 +10,7 @@ import ( "log/slog" "net/http" "net/url" - "os" + "strconv" "strings" "sync" "time" @@ -18,6 +18,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "track-gopher/db" "track-gopher/derby" "track-gopher/web/templates" ) @@ -36,14 +37,16 @@ type Server struct { server *http.Server shutdown chan struct{} logger *slog.Logger + db *db.DB } // NewServer creates a new web server -func NewServer(clock *derby.DerbyClock, events <-chan derby.Event, port int) (*Server, error) { - // Create logger - logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ - Level: slog.LevelDebug, - })) +func NewServer(clock *derby.DerbyClock, events <-chan derby.Event, dbPath string, port int, logger *slog.Logger) (*Server, error) { + // Initialize database + database, err := db.New(dbPath) + if err != nil { + return nil, fmt.Errorf("failed to initialize database: %w", err) + } // Create server s := &Server{ @@ -55,6 +58,7 @@ func NewServer(clock *derby.DerbyClock, events <-chan derby.Event, port int) (*S port: port, shutdown: make(chan struct{}), logger: logger, + db: database, } // Set up routes @@ -110,6 +114,21 @@ func (s *Server) routes() { r.Get("/events", s.handleEvents()) }) + s.router.Get("/admin", s.handleAdmin()) + s.router.Get("/register", s.handleRegister()) + + s.router.Route("/api/groups", func(r chi.Router) { + r.Post("/", s.handleCreateGroup()) + r.Put("/{id}", s.handleUpdateGroup()) + r.Delete("/{id}", s.handleDeleteGroup()) + }) + + s.router.Route("/api/racers", func(r chi.Router) { + r.Post("/", s.handleCreateRacer()) + r.Put("/{id}", s.handleUpdateRacer()) + r.Delete("/{id}", s.handleDeleteRacer()) + }) + // Main page s.router.Get("/", s.handleIndex()) } @@ -135,16 +154,12 @@ func (s *Server) Start() error { // Stop gracefully shuts down the server func (s *Server) Stop() error { - // Signal event forwarder to stop - close(s.shutdown) - - // Close all client connections - s.clientsMux.Lock() - for clientChan := range s.clients { - delete(s.clients, clientChan) - close(clientChan) + // Close database connection + if s.db != nil { + if err := s.db.Close(); err != nil { + s.logger.Error("Error closing database", "error", err) + } } - s.clientsMux.Unlock() // Create a context with timeout for shutdown ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) @@ -360,3 +375,284 @@ func (s *Server) handleEvents() http.HandlerFunc { } } } + +// handleAdmin renders the admin page +func (s *Server) handleAdmin() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Get groups and racers 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 + } + + 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 + } + + // Render template + component := templates.Admin(groups, racers) + if err := component.Render(r.Context(), w); err != nil { + s.logger.Error("Failed to render admin template", "error", err) + http.Error(w, "Failed to render page", http.StatusInternalServerError) + } + } +} + +// handleRegister renders the racer registration page +func (s *Server) handleRegister() 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 + } + + // Render template + component := templates.Register(groups) + if err := component.Render(r.Context(), w); err != nil { + s.logger.Error("Failed to render register template", "error", err) + http.Error(w, "Failed to render page", http.StatusInternalServerError) + } + } +} + +// API handlers for groups + +// handleCreateGroup creates a new group +func (s *Server) handleCreateGroup() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Parse form + if err := r.ParseForm(); err != nil { + s.logger.Error("Failed to parse form", "error", err) + http.Error(w, "Invalid form data", http.StatusBadRequest) + return + } + + // Get form values + name := r.FormValue("name") + description := r.FormValue("description") + + // Validate + if name == "" { + http.Error(w, "Name is required", http.StatusBadRequest) + return + } + + // Create group + id, err := s.db.CreateGroup(name, description) + if err != nil { + s.logger.Error("Failed to create group", "error", err) + http.Error(w, "Failed to create group", http.StatusInternalServerError) + return + } + + // Return success + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{"id":%d}`, id) + } +} + +// handleUpdateGroup updates a group +func (s *Server) handleUpdateGroup() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Get ID from URL + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + http.Error(w, "Invalid ID", http.StatusBadRequest) + return + } + + // Parse form + if err := r.ParseForm(); err != nil { + s.logger.Error("Failed to parse form", "error", err) + http.Error(w, "Invalid form data", http.StatusBadRequest) + return + } + + // Get form values + name := r.FormValue("name") + description := r.FormValue("description") + + // Validate + if name == "" { + http.Error(w, "Name is required", http.StatusBadRequest) + return + } + + // Update group + if err := s.db.UpdateGroup(id, name, description); err != nil { + s.logger.Error("Failed to update group", "error", err) + http.Error(w, "Failed to update group", http.StatusInternalServerError) + return + } + + // Return success + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{"id":%d}`, id) + } +} + +// handleDeleteGroup deletes a group +func (s *Server) handleDeleteGroup() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Get ID from URL + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + http.Error(w, "Invalid ID", http.StatusBadRequest) + return + } + + // Delete group + if err := s.db.DeleteGroup(id); err != nil { + s.logger.Error("Failed to delete group", "error", err) + http.Error(w, "Failed to delete group", http.StatusInternalServerError) + return + } + + // Return success + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{"success":true}`) + } +} + +// API handlers for racers + +// handleCreateRacer creates a new racer +func (s *Server) handleCreateRacer() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Parse form + if err := r.ParseForm(); err != nil { + s.logger.Error("Failed to parse form", "error", err) + http.Error(w, "Invalid form data", http.StatusBadRequest) + return + } + + // Get form values + firstName := r.FormValue("first_name") + lastName := r.FormValue("last_name") + carNumber := r.FormValue("car_number") + carWeightStr := r.FormValue("car_weight") + groupIDStr := r.FormValue("group_id") + + // Validate + if firstName == "" || lastName == "" || carNumber == "" || carWeightStr == "" || groupIDStr == "" { + http.Error(w, "All fields are required", http.StatusBadRequest) + return + } + + // Parse numeric values + carWeight, err := strconv.ParseFloat(carWeightStr, 64) + if err != nil { + http.Error(w, "Invalid car weight", http.StatusBadRequest) + return + } + + groupID, err := strconv.ParseInt(groupIDStr, 10, 64) + if err != nil { + http.Error(w, "Invalid group ID", http.StatusBadRequest) + return + } + + // Create racer + id, err := s.db.CreateRacer(firstName, lastName, carNumber, carWeight, groupID) + if err != nil { + s.logger.Error("Failed to create racer", "error", err) + http.Error(w, "Failed to create racer", http.StatusInternalServerError) + return + } + + // Return success + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{"id":%d}`, id) + } +} + +// handleUpdateRacer updates a racer +func (s *Server) handleUpdateRacer() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Get ID from URL + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + http.Error(w, "Invalid ID", http.StatusBadRequest) + return + } + + // Parse form + if err := r.ParseForm(); err != nil { + s.logger.Error("Failed to parse form", "error", err) + http.Error(w, "Invalid form data", http.StatusBadRequest) + return + } + + // Get form values + firstName := r.FormValue("first_name") + lastName := r.FormValue("last_name") + carNumber := r.FormValue("car_number") + carWeightStr := r.FormValue("car_weight") + groupIDStr := r.FormValue("group_id") + + // Validate + if firstName == "" || lastName == "" || carNumber == "" || carWeightStr == "" || groupIDStr == "" { + http.Error(w, "All fields are required", http.StatusBadRequest) + return + } + + // Parse numeric values + carWeight, err := strconv.ParseFloat(carWeightStr, 64) + if err != nil { + http.Error(w, "Invalid car weight", http.StatusBadRequest) + return + } + + groupID, err := strconv.ParseInt(groupIDStr, 10, 64) + if err != nil { + http.Error(w, "Invalid group ID", http.StatusBadRequest) + return + } + + // Update racer + if err := s.db.UpdateRacer(id, firstName, lastName, carNumber, carWeight, groupID); err != nil { + s.logger.Error("Failed to update racer", "error", err) + http.Error(w, "Failed to update racer", http.StatusInternalServerError) + return + } + + // Return success + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{"id":%d}`, id) + } +} + +// handleDeleteRacer deletes a racer +func (s *Server) handleDeleteRacer() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Get ID from URL + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + http.Error(w, "Invalid ID", http.StatusBadRequest) + return + } + + // Delete racer + if err := s.db.DeleteRacer(id); err != nil { + s.logger.Error("Failed to delete racer", "error", err) + http.Error(w, "Failed to delete racer", http.StatusInternalServerError) + return + } + + // Return success + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{"success":true}`) + } +} diff --git a/web/templates/admin.templ b/web/templates/admin.templ new file mode 100644 index 0000000..f8c5145 --- /dev/null +++ b/web/templates/admin.templ @@ -0,0 +1,358 @@ +package templates + +import ( + "fmt" + "track-gopher/models" +) + +templ Admin(groups []models.Group, racers []models.Racer) { + + +
+ + +| Name | +Description | +Actions | +
|---|---|---|
| { group.Name } | +{ group.Description } | ++ + + | +
| Name | +Car # | +Weight | +Group | +Actions | +
|---|---|---|---|---|
| { racer.FirstName } { racer.LastName } | +{ racer.CarNumber } | +{ fmt.Sprintf("%.1f", racer.CarWeight) } oz | +{ racer.GroupName } | ++ + + | +
| Name | Description | Actions |
|---|---|---|
| ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(group.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/admin.templ`, Line: 55, Col: 28} + } + _, 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, 3, " | ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(group.Description) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/admin.templ`, Line: 56, Col: 35} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " |
| Name | Car # | Weight | Group | Actions |
|---|---|---|---|---|
| ") + 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/admin.templ`, Line: 103, Col: 33} + } + _, 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, 12, " ") + 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/admin.templ`, Line: 103, Col: 52} + } + _, 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, 13, " | ") + 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/admin.templ`, Line: 104, Col: 33} + } + _, 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, 14, " | ") + 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", racer.CarWeight)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/admin.templ`, Line: 105, Col: 54} + } + _, 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, 15, " oz | ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(racer.GroupName) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates/admin.templ`, Line: 106, Col: 33} + } + _, 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, 16, " |
The racer has been registered successfully.
+There was an error registering the racer.
+There was an error registering the racer.