diff --git a/handlers/account.go b/handlers/account.go index 5d1504d..c1ec735 100644 --- a/handlers/account.go +++ b/handlers/account.go @@ -37,6 +37,15 @@ func Login(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusSeeOther) } +func Logout(w http.ResponseWriter, r *http.Request) { + session, _ := GetSession(w, r) + + session.Options.MaxAge = -1 + session.Save(r, w) + + http.Redirect(w, r, "/login", http.StatusSeeOther) +} + func Signup(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { tmpl := template.Must(template.ParseFiles( diff --git a/handlers/draw_handler.go b/handlers/draw_handler.go index 156bcb7..e8cf0cd 100644 --- a/handlers/draw_handler.go +++ b/handlers/draw_handler.go @@ -1,29 +1,67 @@ package handlers import ( + "database/sql" "html/template" "log" "net/http" + "sort" "synlotto-website/helpers" "synlotto-website/models" "github.com/gorilla/csrf" ) -func Home(w http.ResponseWriter, r *http.Request) { - log.Println("✅ Home hit") +func Home(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + log.Println("✅ Home hit") - tmpl := template.Must(template.ParseFiles( - "templates/layout.html", - "templates/index.html", - )) + rows, err := db.Query(` + SELECT id, draw_date, machine, ballset, ball1, ball2, ball3, ball4, ball5, thunderball + FROM results_thunderball + ORDER BY id DESC + `) + if err != nil { + http.Error(w, "Database error", http.StatusInternalServerError) + log.Println("❌ DB error:", err) + return + } - err := tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ - "Data": Draws, - }) - if err != nil { - log.Println("❌ Template error:", err) - http.Error(w, "Error rendering homepage", http.StatusInternalServerError) + var results []models.ThunderballResult + + for rows.Next() { + var res models.ThunderballResult + err := rows.Scan( + &res.Id, &res.DrawDate, &res.Machine, &res.BallSet, + &res.Ball1, &res.Ball2, &res.Ball3, &res.Ball4, &res.Ball5, &res.Thunderball, + ) + if err != nil { + log.Println("❌ Row scan error:", err) + continue + } + + // ✅ Add sorted ball list + res.SortedBalls = []int{ + res.Ball1, res.Ball2, res.Ball3, res.Ball4, res.Ball5, + } + sort.Ints(res.SortedBalls) + + results = append(results, res) + } + + tmpl := template.Must(template.ParseFiles( + "templates/layout.html", + "templates/index.html", + )) + + err = tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ + "Data": results, + }) + if err != nil { + log.Println("❌ Template error:", err) + http.Error(w, "Error rendering homepage", http.StatusInternalServerError) + return + } } } @@ -53,7 +91,7 @@ func Submit(w http.ResponseWriter, r *http.Request) { draw := models.ThunderballResult{ DrawDate: r.FormValue("date"), Machine: r.FormValue("machine"), - Ballset: helpers.Atoi(r.FormValue("ballset")), + BallSet: helpers.Atoi(r.FormValue("ballSet")), Ball1: helpers.Atoi(r.FormValue("ball1")), Ball2: helpers.Atoi(r.FormValue("ball2")), Ball3: helpers.Atoi(r.FormValue("ball3")), @@ -65,7 +103,7 @@ func Submit(w http.ResponseWriter, r *http.Request) { Draws = append(Draws, draw) log.Printf("📅 %s | 🛠 %s | 🎱 %d | 🔢 %d,%d,%d,%d,%d | ⚡ %d\n", - draw.DrawDate, draw.Machine, draw.Ballset, + draw.DrawDate, draw.Machine, draw.BallSet, draw.Ball1, draw.Ball2, draw.Ball3, draw.Ball4, draw.Ball5, draw.Thunderball) http.Redirect(w, r, "/", http.StatusSeeOther) diff --git a/handlers/results.go b/handlers/results.go new file mode 100644 index 0000000..91c4be1 --- /dev/null +++ b/handlers/results.go @@ -0,0 +1,124 @@ +package handlers + +import ( + "database/sql" + "html/template" + "log" + "net/http" + "regexp" + "sort" + "strconv" + "synlotto-website/helpers" + "synlotto-website/models" +) + +func ResultsThunderball(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + const pageSize = 20 + page := 1 + offset := 0 + + query := r.URL.Query().Get("q") + pageStr := r.URL.Query().Get("page") + yearFilter := r.URL.Query().Get("year") + machineFilter := r.URL.Query().Get("machine") + ballSetFilter := r.URL.Query().Get("ballset") + + if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { + page = p + offset = (page - 1) * pageSize + } + + isValidDate := regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`).MatchString + isValidNumber := regexp.MustCompile(`^\d+$`).MatchString + doSearch := isValidDate(query) || isValidNumber(query) + + whereClause := "WHERE 1=1" + args := []interface{}{} + + if doSearch { + whereClause += " AND (draw_date = ? OR id = ?)" + args = append(args, query, query) + } + if yearFilter != "" { + whereClause += " AND strftime('%Y', draw_date) = ?" + args = append(args, yearFilter) + } + if machineFilter != "" { + whereClause += " AND machine = ?" + args = append(args, machineFilter) + } + if ballSetFilter != "" { + whereClause += " AND ballset = ?" + args = append(args, ballSetFilter) + } + + totalPages, totalResults := helpers.GetTotalPages(db, "results_thunderball", whereClause, args, pageSize) + + querySQL := ` + SELECT id, draw_date, machine, ballset, ball1, ball2, ball3, ball4, ball5, thunderball + FROM results_thunderball + ` + whereClause + ` + ORDER BY id DESC + LIMIT ? OFFSET ?` + argsWithLimit := append(args, pageSize, offset) + + rows, err := db.Query(querySQL, argsWithLimit...) + if err != nil { + http.Error(w, "Database error", http.StatusInternalServerError) + log.Println("❌ DB error:", err) + return + } + defer rows.Close() + + var results []models.ThunderballResult + for rows.Next() { + var res models.ThunderballResult + if err := rows.Scan( + &res.Id, &res.DrawDate, &res.Machine, &res.BallSet, + &res.Ball1, &res.Ball2, &res.Ball3, &res.Ball4, &res.Ball5, &res.Thunderball, + ); err != nil { + log.Println("❌ Row scan error:", err) + continue + } + res.SortedBalls = []int{res.Ball1, res.Ball2, res.Ball3, res.Ball4, res.Ball5} + sort.Ints(res.SortedBalls) + results = append(results, res) + } + + years, _ := helpers.GetDistinctValues(db, "strftime('%Y', draw_date)") + machines, _ := helpers.GetDistinctValues(db, "machine") + ballsets, _ := helpers.GetDistinctValues(db, "ballset") + + var noResultsMsg string + if query != "" && !doSearch { + noResultsMsg = "Invalid search. Please enter a draw number or date (yyyy-mm-dd)." + } else if len(results) == 0 && query != "" { + noResultsMsg = "No results found for \"" + query + "\"" + } + + tmpl := template.Must(template.New("").Funcs(helpers.TemplateFuncs()).ParseFiles( + "templates/layout.html", + "templates/results/thunderball.html", + )) + + err = tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ + "Results": results, + "Page": page, + "TotalPages": totalPages, + "TotalResults": totalResults, + "Query": query, + "NoResultsMsg": noResultsMsg, + "Years": years, + "Machines": machines, + "BallSets": ballsets, + "YearFilter": yearFilter, + "MachineFilter": machineFilter, + "BallSetFilter": ballSetFilter, + }) + if err != nil { + log.Println("❌ Template error:", err) + http.Error(w, "Error rendering results", http.StatusInternalServerError) + } + } +} diff --git a/helpers/distinctresults.go b/helpers/distinctresults.go new file mode 100644 index 0000000..8f6df2e --- /dev/null +++ b/helpers/distinctresults.go @@ -0,0 +1,21 @@ +package helpers + +import "database/sql" + +func GetDistinctValues(db *sql.DB, column string) ([]string, error) { + query := "SELECT DISTINCT " + column + " FROM results_thunderball ORDER BY " + column + rows, err := db.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var values []string + for rows.Next() { + var val string + if err := rows.Scan(&val); err == nil { + values = append(values, val) + } + } + return values, nil +} diff --git a/helpers/pagination.go b/helpers/pagination.go new file mode 100644 index 0000000..dbdd7d6 --- /dev/null +++ b/helpers/pagination.go @@ -0,0 +1,18 @@ +package helpers + +import ( + "database/sql" +) + +func GetTotalPages(db *sql.DB, tableName, whereClause string, args []interface{}, pageSize int) (totalPages, totalCount int) { + query := "SELECT COUNT(*) FROM " + tableName + " " + whereClause + row := db.QueryRow(query, args...) + if err := row.Scan(&totalCount); err != nil { + return 1, 0 + } + totalPages = (totalCount + pageSize - 1) / pageSize + if totalPages < 1 { + totalPages = 1 + } + return totalPages, totalCount +} diff --git a/helpers/template.go b/helpers/template.go new file mode 100644 index 0000000..ebeb89d --- /dev/null +++ b/helpers/template.go @@ -0,0 +1,25 @@ +package helpers + +import ( + "html/template" +) + +func TemplateFuncs() template.FuncMap { + return template.FuncMap{ + "plus1": func(i int) int { return i + 1 }, + "minus1": func(i int) int { + if i > 1 { + return i - 1 + } + return 0 + }, + "mul": func(a, b int) int { return a * b }, + "add": func(a, b int) int { return a + b }, + "min": func(a, b int) int { + if a < b { + return a + } + return b + }, + } +} diff --git a/main.go b/main.go index 9eb69ba..0304697 100644 --- a/main.go +++ b/main.go @@ -21,15 +21,19 @@ func main() { mux := http.NewServeMux() - mux.HandleFunc("/", handlers.Home) + mux.HandleFunc("/", handlers.Home(db)) mux.HandleFunc("/new", handlers.NewDraw) mux.HandleFunc("/submit", handlers.Submit) mux.HandleFunc("/ticket", handlers.NewTicket(db)) mux.HandleFunc("/tickets", handlers.ListTickets(db)) mux.HandleFunc("/submit-ticket", handlers.RequireAuth(handlers.SubmitTicket(db))) mux.HandleFunc("/login", handlers.Login) + mux.HandleFunc("/logout", handlers.Logout) mux.HandleFunc("/signup", handlers.Signup) + // Result pages + mux.HandleFunc("/results/thunderball", handlers.ResultsThunderball(db)) + log.Println("🌐 Running on http://localhost:8080") http.ListenAndServe(":8080", csrfMiddleware(mux)) } diff --git a/models/draw.go b/models/draw.go index c845c39..c542f44 100644 --- a/models/draw.go +++ b/models/draw.go @@ -4,11 +4,13 @@ type ThunderballResult struct { Id int DrawDate string Machine string - Ballset int + BallSet int Ball1 int Ball2 int Ball3 int Ball4 int Ball5 int Thunderball int + + SortedBalls []int } diff --git a/storage/insert.go b/storage/insert.go index f2121bf..617bf20 100644 --- a/storage/insert.go +++ b/storage/insert.go @@ -16,7 +16,7 @@ func InsertThunderballResult(db *sql.DB, res models.ThunderballResult) error { ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);` _, err := db.Exec(stmt, - res.DrawDate, res.Machine, res.Ballset, + res.DrawDate, res.Machine, res.BallSet, res.Ball1, res.Ball2, res.Ball3, res.Ball4, res.Ball5, res.Thunderball, ) if err != nil { diff --git a/templates/index.html b/templates/index.html index ed2c399..1b818f2 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2,24 +2,31 @@ + Add New Draw + - {{ if . }} - {{ range . }} - - - - - - - - {{ end }} + + {{ if .Data }} + {{ range .Data }} + + + + + + + + + {{ end }} {{ else }} - + {{ end }}
Draw Number Date Machine Ball Set Balls Thunderball
{{ .Date }}{{ .Machine }}{{ .Ballset }}{{ .Ball1 }}, {{ .Ball2 }}, {{ .Ball3 }}, {{ .Ball4 }}, {{ .Ball5 }}{{ .Thunderball }}
{{ .Id }}{{ .DrawDate }}{{ .Machine }}{{ .BallSet }} + {{ range $i, $n := .SortedBalls }} + {{ if $i }}, {{ end }}{{ $n }} + {{ end }} + {{ .Thunderball }}
No draws recorded yet.
No draws recorded yet.
{{ end }} diff --git a/templates/layout.html b/templates/layout.html index 101abc9..99222ac 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -13,6 +13,7 @@ + Logout

Lotto Tracker

{{ template "content" . }} diff --git a/templates/results/thunderball.html b/templates/results/thunderball.html new file mode 100644 index 0000000..f6e3c01 --- /dev/null +++ b/templates/results/thunderball.html @@ -0,0 +1,98 @@ +{{ define "content" }} +

Thunderball Results

+ +
+ + + + + + + + + + {{ if .Query }} + Clear + {{ end }} +
+ +{{ if .NoResultsMsg }} +

{{ .NoResultsMsg }}

+{{ end }} + + + + + + + + + + + {{ range .Results }} + + + + + + + + + {{ end }} +
Draw NumberDateMachineBall SetNumbersThunderball
{{ .Id }}{{ .DrawDate }}{{ .Machine }}{{ .BallSet }} + {{ range $i, $n := .SortedBalls }} + {{ if $i }}, {{ end }}{{ $n }} + {{ end }} + {{ .Thunderball }}
+ + + + +{{ end }}