diff --git a/pkg/form/submission.go b/pkg/form/submission.go index 7e5513a..88d898f 100644 --- a/pkg/form/submission.go +++ b/pkg/form/submission.go @@ -103,6 +103,8 @@ func (f *Submission) setErrorMessages(err error) { message = "Enter a valid email address." case "eqfield": message = "Does not match." + case "gte": + message = fmt.Sprintf("Must be greater than or equal to %v.", ve.Param()) default: message = "Invalid value." } diff --git a/pkg/handlers/cache.go b/pkg/handlers/cache.go new file mode 100644 index 0000000..acdc5b2 --- /dev/null +++ b/pkg/handlers/cache.go @@ -0,0 +1,90 @@ +package handlers + +import ( + "errors" + "github.com/labstack/echo/v4" + "github.com/mikestefanello/pagoda/pkg/form" + "github.com/mikestefanello/pagoda/pkg/page" + "github.com/mikestefanello/pagoda/pkg/services" + "github.com/mikestefanello/pagoda/templates" + "time" +) + +const ( + routeNameCache = "cache" + routeNameCacheSubmit = "cache.submit" +) + +type ( + Cache struct { + cache *services.CacheClient + *services.TemplateRenderer + } + + cacheForm struct { + Value string `form:"value"` + form.Submission + } +) + +func init() { + Register(new(Cache)) +} + +func (h *Cache) Init(c *services.Container) error { + h.TemplateRenderer = c.TemplateRenderer + h.cache = c.Cache + return nil +} + +func (h *Cache) Routes(g *echo.Group) { + g.GET("/cache", h.Page).Name = routeNameCache + g.POST("/cache", h.Submit).Name = routeNameCacheSubmit +} + +func (h *Cache) Page(ctx echo.Context) error { + p := page.New(ctx) + p.Layout = templates.LayoutMain + p.Name = templates.PageCache + p.Title = "Set a cache entry" + p.Form = form.Get[cacheForm](ctx) + + // Fetch the value from the cache + value, err := h.cache.Get(). + Key("page_cache_example"). + Fetch(ctx.Request().Context()) + + // Store the value in the page so it can be rendered, if found + switch { + case err == nil: + p.Data = value.(string) + case errors.Is(err, services.ErrCacheMiss): + default: + return fail(err, "failed to fetch from cache") + } + + return h.RenderPage(ctx, p) +} + +func (h *Cache) Submit(ctx echo.Context) error { + var input cacheForm + + if err := form.Submit(ctx, &input); err != nil { + return err + } + + // Set the cache + err := h.cache.Set(). + Key("page_cache_example"). + Data(input.Value). + Expiration(10 * time.Minute). + Save(ctx.Request().Context()) + + if err != nil { + return fail(err, "unable to set cache") + } + + form.Clear(ctx) + + return h.Page(ctx) +} diff --git a/pkg/handlers/contact.go b/pkg/handlers/contact.go index 0273364..4c5f264 100644 --- a/pkg/handlers/contact.go +++ b/pkg/handlers/contact.go @@ -2,14 +2,11 @@ package handlers import ( "fmt" - "time" - "github.com/go-playground/validator/v10" "github.com/labstack/echo/v4" "github.com/mikestefanello/pagoda/pkg/form" "github.com/mikestefanello/pagoda/pkg/page" "github.com/mikestefanello/pagoda/pkg/services" - "github.com/mikestefanello/pagoda/pkg/tasks" "github.com/mikestefanello/pagoda/templates" ) @@ -20,8 +17,7 @@ const ( type ( Contact struct { - mail *services.MailClient - tasks *services.TaskClient + mail *services.MailClient *services.TemplateRenderer } @@ -40,7 +36,6 @@ func init() { func (h *Contact) Init(c *services.Container) error { h.TemplateRenderer = c.TemplateRenderer h.mail = c.Mail - h.tasks = c.Tasks return nil } @@ -72,16 +67,6 @@ func (h *Contact) Submit(ctx echo.Context) error { return err } - // TODO create a new page for this - err = h.tasks.New(tasks.ExampleTask{ - Message: input.Message, - }). - Wait(10 * time.Second). - Save() - if err != nil { - return err - } - err = h.mail. Compose(). To(input.Email). diff --git a/pkg/handlers/task.go b/pkg/handlers/task.go new file mode 100644 index 0000000..f4abc64 --- /dev/null +++ b/pkg/handlers/task.go @@ -0,0 +1,88 @@ +package handlers + +import ( + "fmt" + "github.com/mikestefanello/pagoda/pkg/msg" + "time" + + "github.com/go-playground/validator/v10" + "github.com/labstack/echo/v4" + "github.com/mikestefanello/pagoda/pkg/form" + "github.com/mikestefanello/pagoda/pkg/page" + "github.com/mikestefanello/pagoda/pkg/services" + "github.com/mikestefanello/pagoda/pkg/tasks" + "github.com/mikestefanello/pagoda/templates" +) + +const ( + routeNameTask = "task" + routeNameTaskSubmit = "task.submit" +) + +type ( + Task struct { + tasks *services.TaskClient + *services.TemplateRenderer + } + + taskForm struct { + Delay int `form:"delay" validate:"gte=0"` + Message string `form:"message" validate:"required"` + form.Submission + } +) + +func init() { + Register(new(Task)) +} + +func (h *Task) Init(c *services.Container) error { + h.TemplateRenderer = c.TemplateRenderer + h.tasks = c.Tasks + return nil +} + +func (h *Task) Routes(g *echo.Group) { + g.GET("/task", h.Page).Name = routeNameTask + g.POST("/task", h.Submit).Name = routeNameTaskSubmit +} + +func (h *Task) Page(ctx echo.Context) error { + p := page.New(ctx) + p.Layout = templates.LayoutMain + p.Name = templates.PageTask + p.Title = "Create a task" + p.Form = form.Get[taskForm](ctx) + + return h.RenderPage(ctx, p) +} + +func (h *Task) Submit(ctx echo.Context) error { + var input taskForm + + err := form.Submit(ctx, &input) + + switch err.(type) { + case nil: + case validator.ValidationErrors: + return h.Page(ctx) + default: + return err + } + + // Insert the task + err = h.tasks.New(tasks.ExampleTask{ + Message: input.Message, + }). + Wait(time.Duration(input.Delay) * time.Second). + Save() + + if err != nil { + return fail(err, "unable to create a task") + } + + msg.Success(ctx, fmt.Sprintf("The task has been created. Check the logs in %d seconds.", input.Delay)) + form.Clear(ctx) + + return h.Page(ctx) +} diff --git a/templates/layouts/main.gohtml b/templates/layouts/main.gohtml index e256c9c..66f872b 100644 --- a/templates/layouts/main.gohtml +++ b/templates/layouts/main.gohtml @@ -28,6 +28,8 @@
  • {{link (url "home") "Dashboard" .Path}}
  • {{link (url "about") "About" .Path}}
  • {{link (url "contact") "Contact" .Path}}
  • +
  • {{link (url "cache") "Cache" .Path}}
  • +
  • {{link (url "task") "Task" .Path}}
  • diff --git a/templates/pages/cache.gohtml b/templates/pages/cache.gohtml new file mode 100644 index 0000000..a0bb0af --- /dev/null +++ b/templates/pages/cache.gohtml @@ -0,0 +1,36 @@ +{{define "content"}} +
    +
    +
    +

    Test the cache

    +
    +
    + This route handler shows how the default in-memory cache works. Try updating the value using the form below and see how it persists after you reload the page. + HTMX makes it easy to re-render the cached value after the form is submitted. +
    +
    + + + {{if .Data}} + {{.Data}} + {{- else}} + (empty) + {{- end}} +

    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    + + {{template "csrf" .}} +
    +{{end}} \ No newline at end of file diff --git a/templates/pages/task.gohtml b/templates/pages/task.gohtml new file mode 100644 index 0000000..6436c48 --- /dev/null +++ b/templates/pages/task.gohtml @@ -0,0 +1,43 @@ +{{define "content"}} + {{- if not (eq .HTMX.Request.Target "task")}} + + {{- end}} + + {{template "form" .}} +{{end}} + +{{define "form"}} +
    + {{template "messages" .}} +
    + +
    + +
    +

    How long to wait until the task is executed

    + {{template "field-errors" (.Form.GetFieldErrors "Delay")}} +
    + +
    + +
    + +
    +

    The message the task will output to the log

    + {{template "field-errors" (.Form.GetFieldErrors "Message")}} +
    + +
    +
    + +
    +
    + + {{template "csrf" .}} +
    +{{end}} \ No newline at end of file diff --git a/templates/templates.go b/templates/templates.go index bb858bb..ea2ada6 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -22,6 +22,7 @@ const ( const ( PageAbout Page = "about" + PageCache Page = "cache" PageContact Page = "contact" PageError Page = "error" PageForgotPassword Page = "forgot-password" @@ -30,6 +31,7 @@ const ( PageRegister Page = "register" PageResetPassword Page = "reset-password" PageSearch Page = "search" + PageTask Page = "task" ) //go:embed *