feat: add thank you page

This commit is contained in:
Adriano Caloiaro 2025-01-22 12:38:53 -07:00
parent 49b40c0cc5
commit d1d5804d11
No known key found for this signature in database
GPG key ID: C2BC56DE73CE3F75
5 changed files with 744 additions and 636 deletions

View file

@ -112,11 +112,6 @@ func Collect(w http.ResponseWriter, r *http.Request) {
if errs.Any() {
slog.Info("[collector] failed validation", "errors", errs)
w.WriteHeader(http.StatusBadRequest)
} else {
// TODO Redirect to a thank-you page
w.Header().Add("hx-redirect", frm.CollectorPathForm(ctx, *formID))
w.WriteHeader(http.StatusOK)
}
sc := submission.Get("short_code")
submission.Del("short_code")
@ -176,6 +171,11 @@ func Collect(w http.ResponseWriter, r *http.Request) {
return
}
err = ui.ThankYou().Render(ctx, w)
if err != nil {
slog.Error("[collector] unable to render thank you page", "error", err)
w.WriteHeader(http.StatusInternalServerError)
}
}
// validate validates forms

View file

@ -958,6 +958,59 @@ html {
}
}
.form-input,.form-textarea,.form-select,.form-multiselect {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: #fff;
border-color: #6d6d6d;
border-width: 1px;
border-radius: 0px;
padding-top: 0.5rem;
padding-right: 0.75rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
font-size: 1rem;
line-height: 1.5rem;
--tw-shadow: 0 0 #0000;
}
.form-input:focus, .form-textarea:focus, .form-select:focus, .form-multiselect:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #2563eb;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
border-color: #2563eb;
}
.form-input::-moz-placeholder, .form-textarea::-moz-placeholder {
color: #6d6d6d;
opacity: 1;
}
.form-input::placeholder,.form-textarea::placeholder {
color: #6d6d6d;
opacity: 1;
}
.form-input::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
.form-input::-webkit-date-and-time-value {
min-height: 1.5em;
}
.form-input::-webkit-datetime-edit,.form-input::-webkit-datetime-edit-year-field,.form-input::-webkit-datetime-edit-month-field,.form-input::-webkit-datetime-edit-day-field,.form-input::-webkit-datetime-edit-hour-field,.form-input::-webkit-datetime-edit-minute-field,.form-input::-webkit-datetime-edit-second-field,.form-input::-webkit-datetime-edit-millisecond-field,.form-input::-webkit-datetime-edit-meridiem-field {
padding-top: 0;
padding-bottom: 0;
}
.avatar.placeholder > div {
display: flex;
align-items: center;
@ -2793,6 +2846,10 @@ input.tab:checked + .tab-content,
padding-top: 0.75rem;
}
.pt-9 {
padding-top: 2.25rem;
}
.text-center {
text-align: center;
}
@ -2802,6 +2859,11 @@ input.tab:checked + .tab-content,
line-height: 2rem;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.text-5xl {
font-size: 3rem;
line-height: 1;

View file

@ -133,7 +133,7 @@ func SafePath(ctx context.Context, path string) templ.SafeURL {
// head simply provides the <head> element
templ head(pageTitle string) {
<head>
<title>{ ApplicationName } :: { pageTitle }</title>
<title>{ pageTitle }</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
@loadDependenciesOnce.Once() {
@ -269,7 +269,7 @@ templ Builder(form frm.Form) {
// Builder is the primary form builder UI, surrounded by the app chrome
templ Viewer(form frm.Form) {
@App("Form viewer") {
@App(form.Name) {
<div class="bg-sky-100 h-screen">
<section id="app-container" class="container mx-auto">
@FormView(form, false)
@ -846,84 +846,96 @@ templ FormView(form frm.Form, isPreview bool) {
<h1 class="rounded relative mb-2 font-black text-slate-700 text-5xl">
{ form.Name }
</h1>
<form
_="on field_change(field_id, value) formValueChanged(field_id, value)"
>
<input name="submission_id" type="hidden" value="changeme!"/>
if sc := shortCode(ctx); sc != nil {
<input name="short_code" type="hidden" value={ *sc }/>
}
for _, field := range sortFields(form.Fields) {
<div
id={ fmt.Sprintf("field-container-%s", field.ID.String()) }
if field.Hidden {
class="flex flex-col py-3 hidden"
} else {
class="flex flex-col py-3"
}
>
<div id="form-input">
<form
_="on field_change(field_id, value) formValueChanged(field_id, value)"
>
<input name="submission_id" type="hidden" value="changeme!"/>
if sc := shortCode(ctx); sc != nil {
<input name="short_code" type="hidden" value={ *sc }/>
}
for _, field := range sortFields(form.Fields) {
<div
id={ fmt.Sprintf("errors-%s", field.ID.String()) }
class="text-red-400"
></div>
switch field.Type {
case types.FormFieldTypeTextSingle:
@fieldLabel(field)
<input
id={ field.ID.String() }
name={ field.ID.String() }
placeholder={ field.Placeholder }
type="text"
autocomplete="off"
_={ fmt.Sprintf("on keyup debounced at 250ms trigger field_change(field_id: '%s', value: my.value)", field.ID.String()) }
class="flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 shadow-sm focus:outline-none focus:ring-2 focus:border-2 focus:ring-opacity-100 px-4 py-2 text-base resize-y block rounded-xl bg-sky-50"
/>
case types.FormFieldTypeTextMultiple:
@fieldLabel(field)
<textarea
id={ field.ID.String() }
name={ field.ID.String() }
class="flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 shadow-sm focus:outline-none focus:ring-2 focus:border-transparent focus:ring-opacity-100 px-4 py-2 text-base resize-y block rounded-xl bg-sky-50"
placeholder={ field.Placeholder }
autocomplete="off"
_={ fmt.Sprintf("on keyup debounced at 250ms trigger field_change(field_id: '%s', value: my.value)", field.ID.String()) }
rows="3"
></textarea>
case types.FormFieldTypeSingleSelect, types.FormFieldTypeMultiSelect:
@selector.Selector(selector.SelectArgs{
ID: field.ID.String(),
Name: field.ID.String(),
Label: field.Label,
LabelClass: "text-slate-700 text-xl py-1",
Required: field.Required,
Placeholder: field.Placeholder,
Multiple: field.Type == types.FormFieldTypeMultiSelect,
Options: toSelectorOpts(field.Options, false),
SearchDisabled: true,
EditItems: false,
Hyperscript: fmt.Sprintf("on change trigger field_change(field_id: '%s', value: my.value)", field.ID.String()),
})
}
</div>
}
<div class="py-3"></div>
@button(buttonArgs{
Label: "submit",
Type: "submit",
Classes: []string{"flex-grow", "justify-center", "uppercase"},
}, templ.Attributes{
"type": "submit",
"data-hx-post": formCollectorUrl[string](ctx, form),
"data-hx-trigger": "click",
"data-hx-swap": "none",
"data-hx-target-400": "form",
"disabled": isPreview,
},
)
</form>
id={ fmt.Sprintf("field-container-%s", field.ID.String()) }
if field.Hidden {
class="flex flex-col py-3 hidden"
} else {
class="flex flex-col py-3"
}
>
<div
id={ fmt.Sprintf("errors-%s", field.ID.String()) }
class="text-red-400"
></div>
switch field.Type {
case types.FormFieldTypeTextSingle:
@fieldLabel(field)
<input
id={ field.ID.String() }
name={ field.ID.String() }
placeholder={ field.Placeholder }
type="text"
autocomplete="off"
_={ fmt.Sprintf("on keyup debounced at 250ms trigger field_change(field_id: '%s', value: my.value)", field.ID.String()) }
class="flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 shadow-sm focus:outline-none focus:ring-2 focus:border-2 focus:ring-opacity-100 px-4 py-2 text-base resize-y block rounded-xl bg-sky-50"
/>
case types.FormFieldTypeTextMultiple:
@fieldLabel(field)
<textarea
id={ field.ID.String() }
name={ field.ID.String() }
class="flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 shadow-sm focus:outline-none focus:ring-2 focus:border-transparent focus:ring-opacity-100 px-4 py-2 text-base resize-y block rounded-xl bg-sky-50"
placeholder={ field.Placeholder }
autocomplete="off"
_={ fmt.Sprintf("on keyup debounced at 250ms trigger field_change(field_id: '%s', value: my.value)", field.ID.String()) }
rows="3"
></textarea>
case types.FormFieldTypeSingleSelect, types.FormFieldTypeMultiSelect:
@selector.Selector(selector.SelectArgs{
ID: field.ID.String(),
Name: field.ID.String(),
Label: field.Label,
LabelClass: "text-slate-700 text-xl py-1",
Required: field.Required,
Placeholder: field.Placeholder,
Multiple: field.Type == types.FormFieldTypeMultiSelect,
Options: toSelectorOpts(field.Options, false),
SearchDisabled: true,
EditItems: false,
Hyperscript: fmt.Sprintf("on change trigger field_change(field_id: '%s', value: my.value)", field.ID.String()),
})
}
</div>
}
<div class="py-3"></div>
@button(buttonArgs{
Label: "submit",
Type: "submit",
Classes: []string{"flex-grow", "justify-center", "uppercase"},
}, templ.Attributes{
"type": "submit",
"data-hx-post": formCollectorUrl[string](ctx, form),
"data-hx-trigger": "click",
"data-hx-target": "#form-input",
"data-hx-target-400": "form",
"disabled": isPreview,
},
)
</form>
</div>
</div>
}
templ ThankYou() {
@App("Form viewer") {
<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>
}
}
func toSelectorOpts(opts []types.Option, selectAll bool) (sopts []selector.Option) {
// TODO: Fix -- adding an empty option because the first select <option> is selected by default, for some reason
sopts = append(sopts, (selector.Option)(selector.Option{

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,6 @@
<div class=\"flex items-stretch gap-x-2\"><div class=\"inline-flex items-center relative\"><a href=\"#\" class=\"text-sm p-2 hover:bg-gray-100 cursor-pointer rounded-lg text-gray-500 hover:text-gray-800 cursor-pointer\"><span class=\"iconify i-heroicons:question-mark-circle w-5 h-5\" aria-hidden=\"true\"></span></a><!----></div><div class=\"relative inline-flex\"><button type=\"button\" class=\"focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:cursor-not-allowed aria-disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 aria-disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 dark:aria-disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center px-8 md:px-4 py-2\" data-hx-put=\"
\" data-hx-trigger=\"click\" data-hx-swap=\"none\"><!----><svg class=\"w-4 h-4 text-white inline mr-1 -mt-1\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M17 21V13H7V21M7 3V8H15M19 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H16L21 8V19C21 19.5304 20.7893 20.0391 20.4142 20.4142C20.0391 20.7893 19.5304 21 19 21Z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path></svg> Save Form <!----></button><!----></div></div></div>
<head><title>
::
</title><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
<link href=\"
\" rel=\"stylesheet\"><link rel=\"stylesheet\" href=\"
@ -159,7 +158,7 @@ Multi select
</label>
<div id=\"form-viewer\" class=\"flex flex-col w-1/2 mx-auto justify-top pt-12\" data-hx-swap-oob=\"true\"><div id=\"form-metadata\" data-data=\"
\"></div><h1 class=\"rounded relative mb-2 font-black text-slate-700 text-5xl\">
</h1><form _=\"on field_change(field_id, value) formValueChanged(field_id, value)\"><input name=\"submission_id\" type=\"hidden\" value=\"changeme!\">
</h1><div id=\"form-input\"><form _=\"on field_change(field_id, value) formValueChanged(field_id, value)\"><input name=\"submission_id\" type=\"hidden\" value=\"changeme!\">
<input name=\"short_code\" type=\"hidden\" value=\"
\">
<div id=\"
@ -180,4 +179,5 @@ Multi select
\" rows=\"3\"></textarea>
</div>
<div class=\"py-3\"></div>
</form></div>
</form></div></div>
<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>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB