From 300c5c1f1d0d0fd74a9314bd27dac5b8a25cde45 Mon Sep 17 00:00:00 2001 From: Dustin Pianalto Date: Thu, 6 Mar 2025 16:38:44 -0900 Subject: [PATCH] Add admin and registration pages --- db/operations.go | 232 ++++++++++++++++++++ db/schema.go | 116 ++++++++++ examples/main.go | 7 +- go.mod | 1 + go.sum | 2 + models/models.go | 44 ++++ web/server.go | 326 ++++++++++++++++++++++++++-- web/templates/admin.templ | 358 +++++++++++++++++++++++++++++++ web/templates/admin_templ.go | 361 ++++++++++++++++++++++++++++++++ web/templates/register.templ | 128 +++++++++++ web/templates/register_templ.go | 81 +++++++ 11 files changed, 1638 insertions(+), 18 deletions(-) create mode 100644 db/operations.go create mode 100644 db/schema.go create mode 100644 models/models.go create mode 100644 web/templates/admin.templ create mode 100644 web/templates/admin_templ.go create mode 100644 web/templates/register.templ create mode 100644 web/templates/register_templ.go 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) { + + + + + + Derby Race Admin + + + + + + + +
+
+

Derby Race Admin

+ +
+ +
+
+
+
+
Groups
+ +
+
+
+ + + + + + + + + + for _, group := range groups { + + + + + + } + +
NameDescriptionActions
{ group.Name }{ group.Description } + + +
+
+
+
+
+ +
+
+
+
Racers
+
+
+
+ + + + + + + + + + + + for _, racer := range racers { + + + + + + + + } + +
NameCar #WeightGroupActions
{ racer.FirstName } { racer.LastName }{ racer.CarNumber }{ fmt.Sprintf("%.1f", racer.CarWeight) } oz{ racer.GroupName } + + +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + +} \ No newline at end of file diff --git a/web/templates/admin_templ.go b/web/templates/admin_templ.go new file mode 100644 index 0000000..3334f2b --- /dev/null +++ b/web/templates/admin_templ.go @@ -0,0 +1,361 @@ +// 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" + "track-gopher/models" +) + +func Admin(groups []models.Group, 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_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, "Derby Race Admin

Derby Race Admin

Groups
") + 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 + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
NameDescriptionActions
") + 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, "
Racers
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, racer := range racers { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
NameCar #WeightGroupActions
") + 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, "
Add Group
Edit Group
Delete Group

Are you sure you want to delete the group \"\"?

This will also delete all racers in this group!

Edit Racer
Delete Racer

Are you sure you want to delete the racer \"\"?

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/web/templates/register.templ b/web/templates/register.templ new file mode 100644 index 0000000..21ba242 --- /dev/null +++ b/web/templates/register.templ @@ -0,0 +1,128 @@ +package templates + +import ( + "fmt" + "track-gopher/models" +) + +templ Register(groups []models.Group) { + + + + + + Racer Registration + + + + + + + +
+
+

Racer Registration

+ +
+ +
+
+
+
+
Register New Racer
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+
+
+
+ +
+

Registration Successful!

+

The racer has been registered successfully.

+
+
+ + View All Racers +
+
+ +
+

Registration Failed

+

There was an error registering the racer.

+
+ +
+
+
+
+ + + + +} \ No newline at end of file diff --git a/web/templates/register_templ.go b/web/templates/register_templ.go new file mode 100644 index 0000000..767d31f --- /dev/null +++ b/web/templates/register_templ.go @@ -0,0 +1,81 @@ +// 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" + "track-gopher/models" +) + +func Register(groups []models.Group) 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, "Racer Registration

Racer Registration

Register New Racer

Registration Successful!

The racer has been registered successfully.


View All Racers

Registration Failed

There was an error registering the racer.


") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate