feat: give users submission feedback

This commit is contained in:
Adriano Caloiaro 2025-02-14 11:39:22 -07:00
parent f7a5935229
commit a44d713a99
8 changed files with 126 additions and 51 deletions

View file

@ -143,17 +143,10 @@
description = "Execute 'ess' with default parameters"; description = "Execute 'ess' with default parameters";
}; };
frm-server = { frm-dev = {
description = "Run the development server"; description = "Run the development server";
exec = '' exec = ''
go generate ./... && go run ./cmd/dev_server go generate ./... && go run cmd/dev_server/main.go
'';
};
templ-server = {
description = "Run templ and watch for changes";
exec = ''
templ generate -cmd frm-server -watch -watch-pattern='(^(?:[^e]|e[^n]|en[^u]|enu[^m]|enum[^e]|enume[^r])*\.go$)|(.+\.templ$)|(.+_templ\.txt$)|(.+styles\.css$)' -proxy 'http://localhost:3000'
''; '';
}; };
@ -168,19 +161,21 @@
${sqlc}/bin/sqlc generate && echo sqlc generate done ${sqlc}/bin/sqlc generate && echo sqlc generate done
''; '';
}; };
rebuild-tailwind = {
description = "rebuild tailwind's css";
exec = "tailwindcss -c ui/tailwind.config.js -i ./ui/css/tailwind.css -o ./static/css/styles.css && echo -e \"package frm\nimport \\x22fmt\\x22\nfunc main() { fmt.Println($(head -c2 /dev/urandom | od -An -vtu4)) }\" > ./trigger_restart.go";
};
}; };
processes."server" = { processes.frm-server = {
exec = "templ-server"; exec = ''
reflex \
--start-service \
--inverse-regex=testdata \
--inverse-regex='_test.go$' \
--inverse-regex='^\.devenv' \
--inverse-regex='^\.direnv' \
--inverse-regex='^vendor' \
--inverse-regex='.*_enumer\.go|.+\.templ|.+frm-dev$' -v \
frm-dev
'';
process-compose = { process-compose = {
availability = {
restart = "on_failure";
};
readiness_probe = { readiness_probe = {
http_get = { http_get = {
host = "127.0.0.1"; host = "127.0.0.1";
@ -206,11 +201,17 @@
processes.tailwindcss = { processes.tailwindcss = {
exec = '' exec = ''
reflex \ reflex \
-v \ --start-service \
--inverse-regex='^\.devenv' \ -r '.*tailwind\.css$|.*\.templ$' \
--inverse-regex='^\.direnv' \ --inverse-regex='\.devenv' \
-r '.+tailwind\.css$|.+\.templ$' \ --inverse-regex='\.direnv' \
-- rebuild-tailwind -- tailwindcss -i ./css/tailwind.css -o ./static/css/styles.css
'';
};
processes.templ = {
exec = ''
templ generate --watch --proxy="http://localhost:3000"
''; '';
}; };

View file

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"slices" "slices"
"time"
"github.com/acaloiaro/frm" "github.com/acaloiaro/frm"
"github.com/acaloiaro/frm/internal" "github.com/acaloiaro/frm/internal"
@ -163,6 +164,7 @@ func Collect(w http.ResponseWriter, r *http.Request) {
slog.Error("[collector] unable to execute submission receiver", "error", err) slog.Error("[collector] unable to execute submission receiver", "error", err)
} }
} }
time.Sleep(3 * time.Second)
err = collector.ThankYou().Render(ctx, w) err = collector.ThankYou().Render(ctx, w)
if err != nil { if err != nil {
slog.Error("[collector] unable to render thank you page", "error", err) slog.Error("[collector] unable to render thank you page", "error", err)

View file

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"math/rand" "math/rand"
"strings" "strings"
@ -118,6 +119,7 @@ func getPool(ctx context.Context, args DBArgs) (p *pgxpool.Pool, err error) {
func Q(ctx context.Context, args DBArgs) *Queries { func Q(ctx context.Context, args DBArgs) *Queries {
p, err := getPool(ctx, args) p, err := getPool(ctx, args)
if err != nil { if err != nil {
slog.Error("unable to get database connect", "error", err)
return New(&NoopDBTX{}) return New(&NoopDBTX{})
} }
return New(p) return New(p)

View file

@ -2,5 +2,5 @@ package static
import "embed" import "embed"
//go:embed js/* css/* //go:embed js/* css/* img/*
var Assets embed.FS var Assets embed.FS

52
static/img/bars.svg Normal file
View file

@ -0,0 +1,52 @@
<svg width="16" height="16" viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg" fill="#494949">
<rect y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.5s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.5s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="30" y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.25s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.25s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="60" width="15" height="140" rx="6">
<animate attributeName="height"
begin="0s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="90" y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.25s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.25s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="120" y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.5s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.5s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -70,6 +70,8 @@ templ FormView(args ViewerArgs) {
data-hx-post={ formCollectorUrl[string](ctx, args.ShortCode) } data-hx-post={ formCollectorUrl[string](ctx, args.ShortCode) }
data-hx-target-400="#errors" data-hx-target-400="#errors"
} }
data-hx-disabled-elt="find #submit_button"
data-hx-indicator="#spinner"
> >
if args.ShortCode != "" { if args.ShortCode != "" {
<input name="short_code" type="hidden" value={ args.ShortCode }/> <input name="short_code" type="hidden" value={ args.ShortCode }/>
@ -87,14 +89,16 @@ templ FormView(args ViewerArgs) {
</div> </div>
} }
<div class="py-3"></div> <div class="py-3"></div>
<input <button
type="submit" id="submit_button"
value="submit" class="btn bg-primary-500 hover:bg-primary-400 cursor-pointer justify-center uppercase disabled:bg-gray-500"
class="btn bg-primary-500 hover:bg-primary-400 cursor-pointer justify-center uppercase"
if args.Preview { if args.Preview {
disabled disabled
} }
/> >
Submit
<img id="spinner" class="htmx-indicator" src={ frm.CollectorPath(ctx, "/static/img/bars.svg") }/>
</button>
</form> </form>
</div> </div>
if f, err := frm.Instance(ctx); err == nil { if f, err := frm.Instance(ctx); err == nil {

View file

@ -206,7 +206,7 @@ func FormView(args ViewerArgs) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " data-hx-disabled-elt=\"find #submit_button\" data-hx-indicator=\"#spinner\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -218,7 +218,7 @@ func FormView(args ViewerArgs) templ.Component {
var templ_7745c5c3_Var8 string var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(args.ShortCode) templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(args.ShortCode)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/collector/collector.templ`, Line: 75, Col: 66} return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/collector/collector.templ`, Line: 77, Col: 66}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -237,7 +237,7 @@ func FormView(args ViewerArgs) templ.Component {
var templ_7745c5c3_Var9 string var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("field-container-%s", field.ID.String())) templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("field-container-%s", field.ID.String()))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/collector/collector.templ`, Line: 79, Col: 63} return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/collector/collector.templ`, Line: 81, Col: 63}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -271,7 +271,7 @@ func FormView(args ViewerArgs) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div class=\"py-3\"></div><input type=\"submit\" value=\"submit\" class=\"btn bg-primary-500 hover:bg-primary-400 cursor-pointer justify-center uppercase\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div class=\"py-3\"></div><button id=\"submit_button\" class=\"btn bg-primary-500 hover:bg-primary-400 cursor-pointer justify-center uppercase disabled:bg-gray-500\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -281,12 +281,25 @@ func FormView(args ViewerArgs) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "></form></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, ">Submit <img id=\"spinner\" class=\"htmx-indicator\" src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(frm.CollectorPath(ctx, "/static/img/bars.svg"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/collector/collector.templ`, Line: 100, Col: 98}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\"></button></form></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if f, err := frm.Instance(ctx); err == nil { if f, err := frm.Instance(ctx); err == nil {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div id=\"collector_footer\" class=\"pt-6 text-center\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<div id=\"collector_footer\" class=\"pt-6 text-center\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -294,12 +307,12 @@ func FormView(args ViewerArgs) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -332,12 +345,12 @@ func ThankYou() templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var10 := templ.GetChildren(ctx) templ_7745c5c3_Var11 := templ.GetChildren(ctx)
if templ_7745c5c3_Var10 == nil { if templ_7745c5c3_Var11 == nil {
templ_7745c5c3_Var10 = templ.NopComponent templ_7745c5c3_Var11 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var11 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_Var12 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer { if !templ_7745c5c3_IsBuffer {
@ -349,13 +362,13 @@ func ThankYou() templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<div class=\"bg-sky-100 h-screen\"><section id=\"app-container\" class=\"container mx-auto\"><p class=\"text-4xl pt-9\">Thank you!</p></section></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"bg-sky-100 h-screen\"><section id=\"app-container\" class=\"container mx-auto\"><p class=\"text-4xl pt-9\">Thank you!</p></section></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
return nil return nil
}) })
templ_7745c5c3_Err = ui.App("Thank you").Render(templ.WithChildren(ctx, templ_7745c5c3_Var11), templ_7745c5c3_Buffer) templ_7745c5c3_Err = ui.App("Thank you").Render(templ.WithChildren(ctx, templ_7745c5c3_Var12), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -398,12 +411,12 @@ func externalStyles() templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var12 := templ.GetChildren(ctx) templ_7745c5c3_Var13 := templ.GetChildren(ctx)
if templ_7745c5c3_Var12 == nil { if templ_7745c5c3_Var13 == nil {
templ_7745c5c3_Var12 = templ.NopComponent templ_7745c5c3_Var13 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<p class=\"underline\"></p>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<p class=\"underline\"></p>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View file

@ -10,7 +10,7 @@
</h1><div id=\"form-input\" hx-ext=\"response-targets\"><div id=\"errors\"></div><form _=\"on field_change(field_id, value) formValueChanged(field_id, value)\" class=\"flex flex-col gap-3\" </h1><div id=\"form-input\" hx-ext=\"response-targets\"><div id=\"errors\"></div><form _=\"on field_change(field_id, value) formValueChanged(field_id, value)\" class=\"flex flex-col gap-3\"
data-hx-post=\" data-hx-post=\"
\" data-hx-target-400=\"#errors\" \" data-hx-target-400=\"#errors\"
> data-hx-disabled-elt=\"find #submit_button\" data-hx-indicator=\"#spinner\">
<input name=\"short_code\" type=\"hidden\" value=\" <input name=\"short_code\" type=\"hidden\" value=\"
\"> \">
<div id=\" <div id=\"
@ -19,9 +19,10 @@
class=\"flex flex-col bg-sky-200 rounded-xl p-6\" class=\"flex flex-col bg-sky-200 rounded-xl p-6\"
> >
</div> </div>
<div class=\"py-3\"></div><input type=\"submit\" value=\"submit\" class=\"btn bg-primary-500 hover:bg-primary-400 cursor-pointer justify-center uppercase\" <div class=\"py-3\"></div><button id=\"submit_button\" class=\"btn bg-primary-500 hover:bg-primary-400 cursor-pointer justify-center uppercase disabled:bg-gray-500\"
disabled disabled
></form></div> >Submit <img id=\"spinner\" class=\"htmx-indicator\" src=\"
\"></button></form></div>
<div id=\"collector_footer\" class=\"pt-6 text-center\"> <div id=\"collector_footer\" class=\"pt-6 text-center\">
</div> </div>
</div> </div>