mirror of
https://github.com/acaloiaro/frm
synced 2026-07-21 18:29:12 +00:00
feat: add thank you page
This commit is contained in:
parent
49b40c0cc5
commit
d1d5804d11
5 changed files with 744 additions and 636 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
164
ui/common.templ
164
ui/common.templ
|
|
@ -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{
|
||||
|
|
|
|||
1138
ui/common_templ.go
1138
ui/common_templ.go
File diff suppressed because it is too large
Load diff
|
|
@ -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 |
Loading…
Reference in a new issue