add and refine thunderball results page

This commit is contained in:
2025-03-25 14:47:41 +00:00
parent f1ad9757ba
commit 0b93fd75dd
12 changed files with 375 additions and 28 deletions

View File

@@ -37,6 +37,15 @@ func Login(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusSeeOther) 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) { func Signup(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
tmpl := template.Must(template.ParseFiles( tmpl := template.Must(template.ParseFiles(

View File

@@ -1,29 +1,67 @@
package handlers package handlers
import ( import (
"database/sql"
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
"sort"
"synlotto-website/helpers" "synlotto-website/helpers"
"synlotto-website/models" "synlotto-website/models"
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
) )
func Home(w http.ResponseWriter, r *http.Request) { func Home(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println("✅ Home hit") log.Println("✅ Home hit")
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
}
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( tmpl := template.Must(template.ParseFiles(
"templates/layout.html", "templates/layout.html",
"templates/index.html", "templates/index.html",
)) ))
err := tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ err = tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{
"Data": Draws, "Data": results,
}) })
if err != nil { if err != nil {
log.Println("❌ Template error:", err) log.Println("❌ Template error:", err)
http.Error(w, "Error rendering homepage", http.StatusInternalServerError) http.Error(w, "Error rendering homepage", http.StatusInternalServerError)
return
}
} }
} }
@@ -53,7 +91,7 @@ func Submit(w http.ResponseWriter, r *http.Request) {
draw := models.ThunderballResult{ draw := models.ThunderballResult{
DrawDate: r.FormValue("date"), DrawDate: r.FormValue("date"),
Machine: r.FormValue("machine"), Machine: r.FormValue("machine"),
Ballset: helpers.Atoi(r.FormValue("ballset")), BallSet: helpers.Atoi(r.FormValue("ballSet")),
Ball1: helpers.Atoi(r.FormValue("ball1")), Ball1: helpers.Atoi(r.FormValue("ball1")),
Ball2: helpers.Atoi(r.FormValue("ball2")), Ball2: helpers.Atoi(r.FormValue("ball2")),
Ball3: helpers.Atoi(r.FormValue("ball3")), Ball3: helpers.Atoi(r.FormValue("ball3")),
@@ -65,7 +103,7 @@ func Submit(w http.ResponseWriter, r *http.Request) {
Draws = append(Draws, draw) Draws = append(Draws, draw)
log.Printf("📅 %s | 🛠 %s | 🎱 %d | 🔢 %d,%d,%d,%d,%d | ⚡ %d\n", 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) draw.Ball1, draw.Ball2, draw.Ball3, draw.Ball4, draw.Ball5, draw.Thunderball)
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)

124
handlers/results.go Normal file
View File

@@ -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)
}
}
}

View File

@@ -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
}

18
helpers/pagination.go Normal file
View File

@@ -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
}

25
helpers/template.go Normal file
View File

@@ -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
},
}
}

View File

@@ -21,15 +21,19 @@ func main() {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/", handlers.Home) mux.HandleFunc("/", handlers.Home(db))
mux.HandleFunc("/new", handlers.NewDraw) mux.HandleFunc("/new", handlers.NewDraw)
mux.HandleFunc("/submit", handlers.Submit) mux.HandleFunc("/submit", handlers.Submit)
mux.HandleFunc("/ticket", handlers.NewTicket(db)) mux.HandleFunc("/ticket", handlers.NewTicket(db))
mux.HandleFunc("/tickets", handlers.ListTickets(db)) mux.HandleFunc("/tickets", handlers.ListTickets(db))
mux.HandleFunc("/submit-ticket", handlers.RequireAuth(handlers.SubmitTicket(db))) mux.HandleFunc("/submit-ticket", handlers.RequireAuth(handlers.SubmitTicket(db)))
mux.HandleFunc("/login", handlers.Login) mux.HandleFunc("/login", handlers.Login)
mux.HandleFunc("/logout", handlers.Logout)
mux.HandleFunc("/signup", handlers.Signup) mux.HandleFunc("/signup", handlers.Signup)
// Result pages
mux.HandleFunc("/results/thunderball", handlers.ResultsThunderball(db))
log.Println("🌐 Running on http://localhost:8080") log.Println("🌐 Running on http://localhost:8080")
http.ListenAndServe(":8080", csrfMiddleware(mux)) http.ListenAndServe(":8080", csrfMiddleware(mux))
} }

View File

@@ -4,11 +4,13 @@ type ThunderballResult struct {
Id int Id int
DrawDate string DrawDate string
Machine string Machine string
Ballset int BallSet int
Ball1 int Ball1 int
Ball2 int Ball2 int
Ball3 int Ball3 int
Ball4 int Ball4 int
Ball5 int Ball5 int
Thunderball int Thunderball int
SortedBalls []int
} }

View File

@@ -16,7 +16,7 @@ func InsertThunderballResult(db *sql.DB, res models.ThunderballResult) error {
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);` ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);`
_, err := db.Exec(stmt, _, 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, res.Ball1, res.Ball2, res.Ball3, res.Ball4, res.Ball5, res.Thunderball,
) )
if err != nil { if err != nil {

View File

@@ -2,19 +2,26 @@
<a href="/new">+ Add New Draw</a> <a href="/new">+ Add New Draw</a>
<table> <table>
<tr> <tr>
<th>Draw Number</th>
<th>Date</th> <th>Date</th>
<th>Machine</th> <th>Machine</th>
<th>Ball Set</th> <th>Ball Set</th>
<th>Balls</th> <th>Balls</th>
<th>Thunderball</th> <th>Thunderball</th>
</tr> </tr>
{{ if . }}
{{ range . }} {{ if .Data }}
{{ range .Data }}
<tr> <tr>
<td>{{ .Date }}</td> <td>{{ .Id }}</td>
<td>{{ .DrawDate }}</td>
<td>{{ .Machine }}</td> <td>{{ .Machine }}</td>
<td>{{ .Ballset }}</td> <td>{{ .BallSet }}</td>
<td>{{ .Ball1 }}, {{ .Ball2 }}, {{ .Ball3 }}, {{ .Ball4 }}, {{ .Ball5 }}</td> <td>
{{ range $i, $n := .SortedBalls }}
{{ if $i }}, {{ end }}{{ $n }}
{{ end }}
</td>
<td>{{ .Thunderball }}</td> <td>{{ .Thunderball }}</td>
</tr> </tr>
{{ end }} {{ end }}

View File

@@ -13,6 +13,7 @@
</style> </style>
</head> </head>
<body> <body>
<a href="/logout">Logout</a>
<h1>Lotto Tracker</h1> <h1>Lotto Tracker</h1>
{{ template "content" . }} {{ template "content" . }}

View File

@@ -0,0 +1,98 @@
{{ define "content" }}
<h2>Thunderball Results</h2>
<form method="GET" action="/results/thunderball" style="margin-bottom: 1rem;">
<input type="text" name="q" placeholder="Search by date or draw number" value="{{ .Query }}">
<select name="year">
<option value="">All Years</option>
{{ range .Years }}
<option value="{{ . }}" {{ if eq $.YearFilter . }}selected{{ end }}>{{ . }}</option>
{{ end }}
</select>
<select name="machine">
<option value="">All Machines</option>
{{ range .Machines }}
<option value="{{ . }}" {{ if eq $.MachineFilter . }}selected{{ end }}>{{ . }}</option>
{{ end }}
</select>
<select name="ballset">
<option value="">All Ball Sets</option>
{{ range .BallSets }}
<option value="{{ . }}" {{ if eq $.BallSetFilter . }}selected{{ end }}>{{ . }}</option>
{{ end }}
</select>
<button type="submit">Search</button>
{{ if .Query }}
<a href="/results/thunderball" style="margin-left: 10px;">Clear</a>
{{ end }}
</form>
{{ if .NoResultsMsg }}
<p>{{ .NoResultsMsg }}</p>
{{ end }}
<table>
<tr>
<th>Draw Number</th>
<th>Date</th>
<th>Machine</th>
<th>Ball Set</th>
<th>Numbers</th>
<th>Thunderball</th>
</tr>
{{ range .Results }}
<tr>
<td>{{ .Id }}</td>
<td>{{ .DrawDate }}</td>
<td>{{ .Machine }}</td>
<td>{{ .BallSet }}</td>
<td>
{{ range $i, $n := .SortedBalls }}
{{ if $i }}, {{ end }}{{ $n }}
{{ end }}
</td>
<td>{{ .Thunderball }}</td>
</tr>
{{ end }}
</table>
<style>
.pagination a {
margin: 0 5px;
text-decoration: none;
}
.pagination .disabled {
color: #aaa;
pointer-events: none;
cursor: default;
}
</style>
<div class="pagination" style="margin-top: 20px;">
{{ if gt .Page 1 }}
<a href="/results/thunderball?page=1{{ if .Query }}&q={{ .Query }}{{ end }}{{ if .YearFilter }}&year={{ .YearFilter }}{{ end }}{{ if .MachineFilter }}&machine={{ .MachineFilter }}{{ end }}{{ if .BallSetFilter }}&ballset={{ .BallSetFilter }}{{ end }}">« First</a>
<a href="/results/thunderball?page={{ minus1 .Page }}{{ if .Query }}&q={{ .Query }}{{ end }}{{ if .YearFilter }}&year={{ .YearFilter }}{{ end }}{{ if .MachineFilter }}&machine={{ .MachineFilter }}{{ end }}{{ if .BallSetFilter }}&ballset={{ .BallSetFilter }}{{ end }}">← Prev</a>
{{ else }}
<span class="disabled">« First</span>
<span class="disabled">← Prev</span>
{{ end }}
&nbsp; Page {{ .Page }} of {{ .TotalPages }} &nbsp;
{{ if lt .Page .TotalPages }}
<a href="/results/thunderball?page={{ plus1 .Page }}{{ if .Query }}&q={{ .Query }}{{ end }}{{ if .YearFilter }}&year={{ .YearFilter }}{{ end }}{{ if .MachineFilter }}&machine={{ .MachineFilter }}{{ end }}{{ if .BallSetFilter }}&ballset={{ .BallSetFilter }}{{ end }}">Next →</a>
<a href="/results/thunderball?page={{ .TotalPages }}{{ if .Query }}&q={{ .Query }}{{ end }}{{ if .YearFilter }}&year={{ .YearFilter }}{{ end }}{{ if .MachineFilter }}&machine={{ .MachineFilter }}{{ end }}{{ if .BallSetFilter }}&ballset={{ .BallSetFilter }}{{ end }}">Last »</a>
{{ else }}
<span class="disabled">Next →</span>
<span class="disabled">Last »</span>
{{ end }}
<p>
Showing {{ add (mul (minus1 .Page) 20) 1 }}{{ min (mul .Page 20) .TotalResults }} of {{ .TotalResults }} results
</p>
</div>
{{ end }}