├── .air.toml ├── .gitignore ├── cmd └── main.go ├── go.mod ├── go.sum ├── src └── dom-helpers.js └── views └── index.html /.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | testdata_dir = "testdata" 3 | tmp_dir = "tmp" 4 | 5 | [build] 6 | args_bin = [] 7 | bin = "./tmp/main" 8 | cmd = "go build -o ./tmp/main cmd/main.go" 9 | delay = 0 10 | exclude_dir = ["node_modules", "assets", "tmp", "vendor", "testdata"] 11 | exclude_file = [] 12 | exclude_regex = ["_test.go"] 13 | exclude_unchanged = false 14 | follow_symlink = false 15 | full_bin = "" 16 | include_dir = [] 17 | include_ext = ["go", "html", "css", "js"] 18 | include_file = [] 19 | kill_delay = "0s" 20 | log = "build-errors.log" 21 | poll = false 22 | poll_interval = 0 23 | rerun = false 24 | rerun_delay = 500 25 | send_interrupt = false 26 | stop_on_error = false 27 | 28 | [color] 29 | app = "" 30 | build = "yellow" 31 | main = "magenta" 32 | runner = "green" 33 | watcher = "cyan" 34 | 35 | [log] 36 | main_only = false 37 | time = false 38 | 39 | [misc] 40 | clean_on_exit = false 41 | 42 | [screen] 43 | clear_on_rebuild = false 44 | keep_scroll = true 45 | 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tmp/ 2 | 3 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "html/template" 5 | "io" 6 | "net/mail" 7 | "time" 8 | 9 | "github.com/labstack/echo/v4" 10 | "github.com/labstack/echo/v4/middleware" 11 | ) 12 | 13 | type Template struct { 14 | tmpls *template.Template 15 | } 16 | 17 | func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { 18 | return t.tmpls.ExecuteTemplate(w, name, data) 19 | } 20 | 21 | type IndexPage struct { 22 | Email string 23 | ErrorMsgs map[string]string 24 | } 25 | 26 | func validEmail(email string) bool { 27 | _, err := mail.ParseAddress(email) 28 | return err == nil 29 | } 30 | 31 | func main() { 32 | e := echo.New() 33 | 34 | e.Renderer = &Template{ 35 | tmpls: template.Must(template.ParseGlob("views/*.html")), 36 | } 37 | 38 | e.Use(middleware.Logger()) 39 | e.Static("/src", "src") 40 | 41 | e.GET("/", func(c echo.Context) error { 42 | email := c.QueryParam("email") 43 | return c.Render(200, "index", IndexPage{Email: email}) 44 | }) 45 | 46 | e.POST("/subscribe", func(c echo.Context) error { 47 | time.Sleep(1 * time.Second) 48 | email := c.FormValue("email") 49 | if !validEmail(email) { 50 | c.Logger().Error("Invalid email address: " + email) 51 | return c.Render(422, "index", IndexPage{ 52 | Email: email, 53 | ErrorMsgs: map[string]string{ 54 | "email": "Invalid email address", 55 | }, 56 | }) 57 | } 58 | 59 | return c.Redirect(303, "/subscribe?email="+email) 60 | }) 61 | 62 | e.GET("/subscribe", func(c echo.Context) error { 63 | email := c.FormValue("email") 64 | return c.Render(200, "thanks", IndexPage{Email: email}) 65 | }) 66 | 67 | e.Logger.Fatal(e.Start(":42069")) 68 | } 69 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module theprimeagen.tv/subscribe 2 | 3 | go 1.21.0 4 | 5 | require github.com/labstack/echo/v4 v4.11.3 6 | 7 | require ( 8 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 9 | github.com/labstack/gommon v0.4.0 // indirect 10 | github.com/mattn/go-colorable v0.1.13 // indirect 11 | github.com/mattn/go-isatty v0.0.19 // indirect 12 | github.com/valyala/bytebufferpool v1.0.0 // indirect 13 | github.com/valyala/fasttemplate v1.2.2 // indirect 14 | golang.org/x/crypto v0.14.0 // indirect 15 | golang.org/x/net v0.17.0 // indirect 16 | golang.org/x/sys v0.13.0 // indirect 17 | golang.org/x/text v0.13.0 // indirect 18 | golang.org/x/time v0.3.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 5 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 6 | github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM= 7 | github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= 8 | github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= 9 | github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= 10 | github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 11 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 12 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 13 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 14 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 15 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 16 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 21 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 22 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 23 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 24 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 25 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 26 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 27 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 28 | golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= 29 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 30 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 31 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 32 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 35 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 37 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 38 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 40 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 41 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 42 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 44 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 46 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 47 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 48 | -------------------------------------------------------------------------------- /src/dom-helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {HTMLFormElement} form 3 | */ 4 | function disableSubmit(form) { 5 | /** @type HTMLButtonElement */ 6 | const button = form.querySelector("button[type=submit]"); 7 | 8 | if (!button) { 9 | throw new Error("form without button"); 10 | } 11 | 12 | // ensure at load time that button is not disabled 13 | button.removeAttribute("disabled"); 14 | 15 | form.addEventListener("htmx:beforeRequest", function() { 16 | button.toggleAttribute("disabled", true); 17 | }) 18 | 19 | form.addEventListener("htmx:beforeSwap", function() { 20 | button.toggleAttribute("disabled", true); 21 | }) 22 | } 23 | 24 | // on document ready 25 | document.addEventListener("DOMContentLoaded", function() { 26 | console.log("here"); 27 | document.body.addEventListener('htmx:beforeSwap', function(evt) { 28 | console.log("here 422"); 29 | if (evt.detail.xhr.status === 422) { 30 | console.log("here 422 YES"); 31 | evt.detail.shouldSwap = true; 32 | evt.detail.isError = false; 33 | } 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | {{ block "scripts" . }} 2 | 3 | 4 | 5 | {{ end }} 6 | 7 | {{ block "index" . }} 8 | 9 | 10 |
11 |Don't Miss The Latest Updates
42 | 62 |75 | Thanks, {{ .Email }}, for subscribing to ThePrimeagen and HTMX updates 76 |
77 |