From d838cee9cb536049119a22e805746d958628387d Mon Sep 17 00:00:00 2001 From: Thomas Voss Date: Mon, 4 Aug 2025 01:23:32 +0200 Subject: Big changes to everything --- src/countries.go | 29 ++ src/dbx/mintages.go | 135 +++++++- src/http.go | 63 +++- src/templates.go | 45 ++- src/templates/-404.html.tmpl | 4 +- src/templates/-base.html.tmpl | 4 +- src/templates/-navbar.html.tmpl | 19 +- src/templates/banknotes-codes.html.tmpl | 535 ++++++++++++++++++----------- src/templates/banknotes.html.tmpl | 74 ++-- src/templates/coins-mintages.html.tmpl | 310 ++++++++++++----- src/templates/coins.html.tmpl | 74 ++-- src/templates/collecting-crh.html.tmpl | 590 ++++++++++++++++---------------- src/templates/collecting.html.tmpl | 96 +++--- src/templates/index.html.tmpl | 4 +- src/templates/jargon.html.tmpl | 11 +- src/templates/language.html.tmpl | 6 +- 16 files changed, 1247 insertions(+), 752 deletions(-) (limited to 'src') diff --git a/src/countries.go b/src/countries.go index 657cac8..d168266 100644 --- a/src/countries.go +++ b/src/countries.go @@ -13,6 +13,35 @@ type country struct { Code, Name string } +var countryCodeToName = map[string]string{ + "ad": "Andorra", + "at": "Austria", + "be": "Belgium", + /* TODO(2026): Add Bulgaria */ + /* "bg": "Bulgaria", */ + "cy": "Cyprus", + "de": "Germany", + "ee": "Estonia", + "es": "Spain", + "fi": "Finland", + "fr": "France", + "gr": "Greece", + "hr": "Croatia", + "ie": "Ireland", + "it": "Italy", + "lt": "Lithuania", + "lu": "Luxembourg", + "lv": "Latvia", + "mc": "Monaco", + "mt": "Malta", + "nl": "Netherlands", + "pt": "Portugal", + "si": "Slovenia", + "sk": "Slovakia", + "sm": "San Marino", + "va": "Vatican City", +} + func sortedCountries(p i18n.Printer) []country { xs := []country{ {Code: "ad", Name: p.GetC("Andorra", "Place Name")}, diff --git a/src/dbx/mintages.go b/src/dbx/mintages.go index 5b226bd..9b98bd6 100644 --- a/src/dbx/mintages.go +++ b/src/dbx/mintages.go @@ -5,9 +5,14 @@ import ( "slices" ) -type MintageData struct { - Standard []MSRow - Commemorative []MCRow +type CountryMintageData struct { + Standard []MSCountryRow + Commemorative []MCCountryRow +} + +type YearMintageData struct { + Standard []MSYearRow + Commemorative []MCYearRow } type msRowInternal struct { @@ -31,14 +36,14 @@ type mcRowInternal struct { Reference sql.Null[string] } -type MSRow struct { +type MSCountryRow struct { Year int Mintmark string Mintages [ndenoms]int References []string } -type MCRow struct { +type MCCountryRow struct { Year int Name string Number int @@ -47,6 +52,22 @@ type MCRow struct { Reference string } +type MSYearRow struct { + Country string + Mintmark string + Mintages [ndenoms]int + References []string +} + +type MCYearRow struct { + Country string + Name string + Number int + Mintmark string + Mintage int + Reference string +} + type MintageType int /* DO NOT REORDER! */ @@ -78,11 +99,101 @@ func NewMintageType(s string) MintageType { return TypeCirc } -func GetMintages(country string, typ MintageType) (MintageData, error) { +func GetMintagesByYear(year int, typ MintageType) (YearMintageData, error) { + var ( + zero YearMintageData + xs []MSYearRow + ys []MCYearRow + ) + + rs, err := db.Queryx(` + SELECT * FROM mintages_s + WHERE year = ? AND type = ? + ORDER BY country, mintmark, denomination + `, year, typ) + if err != nil { + return zero, err + } + + for rs.Next() { + var x msRowInternal + if err = rs.StructScan(&x); err != nil { + return zero, err + } + + loop: + msr := MSYearRow{ + Country: x.Country, + Mintmark: sqlOr(x.Mintmark, ""), + References: make([]string, 0, ndenoms), + } + for i := range msr.Mintages { + msr.Mintages[i] = MintageUnknown + } + msr.Mintages[denomToIdx(x.Denomination)] = + sqlOr(x.Mintage, MintageUnknown) + if x.Reference.Valid { + msr.References = append(msr.References, x.Reference.V) + } + + for rs.Next() { + var y msRowInternal + if err = rs.StructScan(&y); err != nil { + return zero, err + } + + if x.Country != y.Country || x.Mintmark != y.Mintmark { + x = y + xs = append(xs, msr) + goto loop + } + + msr.Mintages[denomToIdx(y.Denomination)] = + sqlOr(y.Mintage, MintageUnknown) + if y.Reference.Valid { + msr.References = append(msr.References, y.Reference.V) + } + } + + xs = append(xs, msr) + } + + if err = rs.Err(); err != nil { + return zero, err + } + + rs, err = db.Queryx(` + SELECT * FROM mintages_c + WHERE year = ? AND type = ? + ORDER BY country, mintmark, number + `, year, typ) + if err != nil { + return zero, err + } + + for rs.Next() { + var y mcRowInternal + if err = rs.StructScan(&y); err != nil { + return zero, err + } + ys = append(ys, MCYearRow{ + Country: y.Country, + Name: y.Name, + Number: y.Number, + Mintmark: sqlOr(y.Mintmark, ""), + Mintage: sqlOr(y.Mintage, MintageUnknown), + Reference: sqlOr(y.Reference, ""), + }) + } + + return YearMintageData{xs, ys}, nil +} + +func GetMintagesByCountry(country string, typ MintageType) (CountryMintageData, error) { var ( - zero MintageData - xs []MSRow - ys []MCRow + zero CountryMintageData + xs []MSCountryRow + ys []MCCountryRow ) rs, err := db.Queryx(` @@ -101,7 +212,7 @@ func GetMintages(country string, typ MintageType) (MintageData, error) { } loop: - msr := MSRow{ + msr := MSCountryRow{ Year: x.Year, Mintmark: sqlOr(x.Mintmark, ""), References: make([]string, 0, ndenoms), @@ -155,7 +266,7 @@ func GetMintages(country string, typ MintageType) (MintageData, error) { if err = rs.StructScan(&y); err != nil { return zero, err } - ys = append(ys, MCRow{ + ys = append(ys, MCCountryRow{ Year: y.Year, Name: y.Name, Number: y.Number, @@ -165,7 +276,7 @@ func GetMintages(country string, typ MintageType) (MintageData, error) { }) } - return MintageData{xs, ys}, rs.Err() + return CountryMintageData{xs, ys}, rs.Err() } func sqlOr[T any](v sql.Null[T], dflt T) T { diff --git a/src/http.go b/src/http.go index ac6f6da..071b25c 100644 --- a/src/http.go +++ b/src/http.go @@ -13,6 +13,8 @@ import ( "strings" "time" + "golang.org/x/text/collate" + "golang.org/x/text/language" . "git.thomasvoss.com/euro-cash.eu/pkg/try" "git.thomasvoss.com/euro-cash.eu/src/dbx" @@ -38,6 +40,7 @@ func Run(port int) { mux.Handle("GET /storage/", fs) if Debugp { mux.Handle("GET /style.css", fs) + mux.Handle("GET /style-2.css", fs) } else { mux.Handle("GET /style.min.css", fs) } @@ -137,12 +140,6 @@ func countryHandler(next http.Handler) http.Handler { func mintageHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { td := r.Context().Value("td").(*templateData) - td.Code = r.FormValue("code") - if !slices.ContainsFunc(td.Countries, func(c country) bool { - return c.Code == td.Code - }) { - td.Code = td.Countries[0].Code - } td.Type = r.FormValue("type") switch td.Type { @@ -151,8 +148,60 @@ func mintageHandler(next http.Handler) http.Handler { td.Type = "circ" } + td.FilterBy = r.FormValue("filter-by") + switch td.FilterBy { + case "country", "year": + default: + td.FilterBy = "country" + } + var err error - td.Mintages, err = dbx.GetMintages(td.Code, dbx.NewMintageType(td.Type)) + mt := dbx.NewMintageType(td.Type) + + switch td.FilterBy { + case "country": + td.Code = r.FormValue("country") + if !slices.ContainsFunc(td.Countries, func(c country) bool { + return c.Code == td.Code + }) { + td.Code = td.Countries[0].Code + } + td.CountryMintages, err = dbx.GetMintagesByCountry(td.Code, mt) + case "year": + td.Year, err = strconv.Atoi(r.FormValue("year")) + if err != nil { + td.Year = 1999 + } + td.YearMintages, err = dbx.GetMintagesByYear(td.Year, mt) + + /* NOTE: It’s safe to use MustParse() here, because by this + point we know that all BCPs are valid. */ + c := collate.New(language.MustParse(td.Printer.Bcp)) + for i, r := range td.YearMintages.Standard { + name := td.Printer.GetC( + countryCodeToName[r.Country], "Place Name") + td.YearMintages.Standard[i].Country = name + } + for i, r := range td.YearMintages.Commemorative { + name := td.Printer.GetC( + countryCodeToName[r.Country], "Place Name") + td.YearMintages.Commemorative[i].Country = name + } + slices.SortFunc(td.YearMintages.Standard, func(x, y dbx.MSYearRow) int { + Δ := c.CompareString(x.Country, y.Country) + if Δ == 0 { + Δ = c.CompareString(x.Mintmark, y.Mintmark) + } + return Δ + }) + slices.SortFunc(td.YearMintages.Commemorative, func(x, y dbx.MCYearRow) int { + Δ := c.CompareString(x.Country, y.Country) + if Δ == 0 { + Δ = c.CompareString(x.Mintmark, y.Mintmark) + } + return Δ + }) + } if err != nil { throwError(http.StatusInternalServerError, err, w, r) return diff --git a/src/templates.go b/src/templates.go index 3852b9d..076f8dc 100644 --- a/src/templates.go +++ b/src/templates.go @@ -5,6 +5,7 @@ import ( "io/fs" "log" "strings" + "time" . "git.thomasvoss.com/euro-cash.eu/pkg/try" "git.thomasvoss.com/euro-cash.eu/pkg/watch" @@ -14,12 +15,14 @@ import ( ) type templateData struct { - Debugp bool - Printer i18n.Printer - Printers map[string]i18n.Printer - Code, Type string - Mintages dbx.MintageData - Countries []country + Debugp bool + Printer i18n.Printer + Printers map[string]i18n.Printer + Code, Type, FilterBy string + Year int + CountryMintages dbx.CountryMintageData + YearMintages dbx.YearMintageData + Countries []country } var ( @@ -30,16 +33,22 @@ var ( "ifElse": ifElse, "locales": i18n.Locales, "map": templateMakeMap, + "plus1": plus1, "safe": asHTML, "toString": toString, "toUpper": strings.ToUpper, "tuple": templateMakeTuple, "withTranslation": withTranslation, "withTranslations": withTranslations, + "years": years, } ) func BuildTemplates(dir fs.FS) { + buildTemplates(dir, Debugp) +} + +func buildTemplates(dir fs.FS, debugp bool) { ents := Try2(fs.ReadDir(dir, ".")) notFoundTmpl = buildTemplate(dir, "-404") errorTmpl = buildTemplate(dir, "-error") @@ -51,7 +60,7 @@ func BuildTemplates(dir fs.FS) { } name := e.Name() buildAndSetTemplate(dir, name) - if Debugp { + if debugp { go watch.FileFS(dir, name, func() { defer func() { if p := recover(); p != nil { @@ -59,8 +68,13 @@ func BuildTemplates(dir fs.FS) { } }() - buildAndSetTemplate(dir, name) - log.Printf("Template ‘%s’ updated\n", name) + if strings.HasPrefix(name, "-") { + buildTemplates(dir, false) + log.Println("All templates updated") + } else { + buildAndSetTemplate(dir, name) + log.Printf("Template ‘%s’ updated\n", name) + } }) } } @@ -185,6 +199,19 @@ func withTranslations(tag string, text string, translations ...[]any) template.H return template.HTML(bob.String()) } +func plus1(x int) int { + return x + 1 +} + +func years() []int { + sy, ey := 1999, time.Now().Year() + xs := make([]int, ey-sy+1) + for i := range xs { + xs[i] = sy + i + } + return xs +} + func (td templateData) Get(fmt string, args ...map[string]any) template.HTML { return template.HTML(td.Printer.Get(fmt, args...)) } diff --git a/src/templates/-404.html.tmpl b/src/templates/-404.html.tmpl index b149777..fd17cf6 100644 --- a/src/templates/-404.html.tmpl +++ b/src/templates/-404.html.tmpl @@ -3,11 +3,11 @@ {{ end }} {{ define "content" }} -
+
{{ template "navbar" . }}

{{ .Get "Page Not Found" }}

-
+

{{ .Get "The page you were looking for does not exist. If you believe this is a mistake then don’t hesitate to contact ‘@onetruemangoman’ on Discord or email us at {Email:e}." (map "Email" "mail@euro-cash.eu") }} diff --git a/src/templates/-base.html.tmpl b/src/templates/-base.html.tmpl index 62f3301..f455a14 100644 --- a/src/templates/-base.html.tmpl +++ b/src/templates/-base.html.tmpl @@ -4,7 +4,7 @@ {{ if .Debugp }} - + {{ else }} {{ end }} @@ -38,7 +38,7 @@ {{ template "content" . }} -