From f33e5926c043db07a1961fdde614a40851d40d09 Mon Sep 17 00:00:00 2001
From: Adriano Caloiaro ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "\" data-hx-swap-oob=\"true\" class=\"flex flex-col gap-3 py-3\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- var templ_7745c5c3_Var55 string
- templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(err.Error())
+ var templ_7745c5c3_Var59 string
+ templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(err.Error())
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/common.templ`, Line: 370, Col: 18}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/common.templ`, Line: 425, Col: 18}
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "
Date: Wed, 26 Mar 2025 15:29:25 -0600
Subject: [PATCH] feat: add toast when form is saved
---
handlers/builder.go | 45 +-
static/css/styles.css | 198 +++++++
ui/builder/builder.templ | 4 +-
ui/builder/builder_templ.go | 4 +-
ui/builder/builder_templ.txt | 82 +++
ui/collector/collector_templ.txt | 24 +
ui/common.templ | 55 ++
ui/common_templ.go | 858 +++++++++++++++++-------------
ui/common_templ.txt | 84 +++
ui/errors_templ.txt | 3 +
ui/fields/fields_templ.txt | 46 ++
ui/fields/single_choice_templ.txt | 28 +
ui/selector/selector_templ.txt | 25 +
ui/trigger_rebuild_templ.txt | 1 +
14 files changed, 1076 insertions(+), 381 deletions(-)
create mode 100644 ui/builder/builder_templ.txt
create mode 100644 ui/collector/collector_templ.txt
create mode 100644 ui/common_templ.txt
create mode 100644 ui/errors_templ.txt
create mode 100644 ui/fields/fields_templ.txt
create mode 100644 ui/fields/single_choice_templ.txt
create mode 100644 ui/selector/selector_templ.txt
create mode 100644 ui/trigger_rebuild_templ.txt
diff --git a/handlers/builder.go b/handlers/builder.go
index 6653c00..dab0627 100644
--- a/handlers/builder.go
+++ b/handlers/builder.go
@@ -13,6 +13,7 @@ import (
"github.com/acaloiaro/frm/internal"
"github.com/acaloiaro/frm/static"
"github.com/acaloiaro/frm/types"
+ "github.com/acaloiaro/frm/ui"
"github.com/acaloiaro/frm/ui/builder"
"github.com/acaloiaro/frm/ui/collector"
"github.com/google/uuid"
@@ -699,20 +700,34 @@ func PublishDraft(w http.ResponseWriter, r *http.Request) {
f, err := frm.Instance(ctx)
if err != nil {
slog.Error("unable to publish draft", "error", err)
+ ui.Toast(ui.ToastArgs{
+ Position: ui.ToastPositionTop,
+ Type: ui.ToastTypeError,
+ Message: "Failed! Your form was not saved.",
+ }).Render(ctx, w)
w.WriteHeader(http.StatusInternalServerError)
return
}
draftID, err := formID(ctx, f)
if err != nil {
slog.Error("unable to publish draft", "error", err)
- w.WriteHeader(http.StatusNotFound)
+ ui.Toast(ui.ToastArgs{
+ Position: ui.ToastPositionTop,
+ Type: ui.ToastTypeError,
+ Message: "Failed! Your form was not saved.",
+ }).Render(ctx, w)
+ w.WriteHeader(http.StatusInternalServerError)
return
}
tx, err := internal.Tx(ctx, f.DBArgs)
if err != nil {
slog.Error("unable to publish draft", "error", err)
- w.WriteHeader(http.StatusNotFound)
+ ui.Toast(ui.ToastArgs{
+ Position: ui.ToastPositionTop,
+ Type: ui.ToastTypeError,
+ Message: "Failed! Your form was not saved.",
+ }).Render(ctx, w)
return
}
defer func() {
@@ -723,6 +738,11 @@ func PublishDraft(w http.ResponseWriter, r *http.Request) {
_, err = q.PublishDraft(ctx, *draftID)
if err != nil {
slog.Error("unable to publish draft", "error", err)
+ ui.Toast(ui.ToastArgs{
+ Position: ui.ToastPositionTop,
+ Type: ui.ToastTypeError,
+ Message: "Failed! Your form was not saved.",
+ }).Render(ctx, w)
w.WriteHeader(http.StatusInternalServerError)
return
}
@@ -733,13 +753,32 @@ func PublishDraft(w http.ResponseWriter, r *http.Request) {
})
if err != nil {
slog.Error("unable to publish draft", "error", err)
+ ui.Toast(ui.ToastArgs{
+ Position: ui.ToastPositionTop,
+ Type: ui.ToastTypeError,
+ Message: "Failed! Your form was not saved.",
+ }).Render(ctx, w)
w.WriteHeader(http.StatusInternalServerError)
return
}
err = tx.Commit(ctx)
+ if err != nil {
+ slog.Error("unable to publish draft", "error", err)
+ ui.Toast(ui.ToastArgs{
+ Position: ui.ToastPositionTop,
+ Type: ui.ToastTypeError,
+ Message: "Failed! Your form was not saved.",
+ }).Render(ctx, w)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
- w.WriteHeader(http.StatusNoContent)
+ ui.Toast(ui.ToastArgs{
+ Position: ui.ToastPositionTop,
+ Type: ui.ToastTypeSuccess,
+ Message: "Success! Your form has been saved.",
+ }).Render(ctx, w)
}
// DeleteForm deletes forms
diff --git a/static/css/styles.css b/static/css/styles.css
index 73d522f..3371c4f 100644
--- a/static/css/styles.css
+++ b/static/css/styles.css
@@ -1152,6 +1152,57 @@ html {
content: var(--tw-content);
}
+.card {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ border-radius: var(--rounded-box, 1rem);
+}
+
+.card:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+}
+
+.card figure {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.card.image-full {
+ display: grid;
+}
+
+.card.image-full:before {
+ position: relative;
+ content: "";
+ z-index: 10;
+ border-radius: var(--rounded-box, 1rem);
+ --tw-bg-opacity: 1;
+ background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
+ opacity: 0.75;
+}
+
+.card.image-full:before,
+ .card.image-full > * {
+ grid-column-start: 1;
+ grid-row-start: 1;
+}
+
+.card.image-full > figure img {
+ height: 100%;
+ -o-object-fit: cover;
+ object-fit: cover;
+}
+
+.card.image-full > .card-body {
+ position: relative;
+ z-index: 20;
+ --tw-text-opacity: 1;
+ color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));
+}
+
.checkbox {
flex-shrink: 0;
--chkbg: var(--fallback-bc,oklch(var(--bc)/1));
@@ -1611,6 +1662,17 @@ input.tab:checked + .tab-content,
background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
}
+.toast {
+ position: fixed;
+ display: flex;
+ min-width: -moz-fit-content;
+ min-width: fit-content;
+ flex-direction: column;
+ white-space: nowrap;
+ gap: 0.5rem;
+ padding: 1rem;
+}
+
.toggle {
flex-shrink: 0;
--tglbg: var(--fallback-b1,oklch(var(--b1)/1));
@@ -1738,6 +1800,44 @@ input.tab:checked + .tab-content,
}
}
+.card :where(figure:first-child) {
+ overflow: hidden;
+ border-start-start-radius: inherit;
+ border-start-end-radius: inherit;
+ border-end-start-radius: unset;
+ border-end-end-radius: unset;
+}
+
+.card :where(figure:last-child) {
+ overflow: hidden;
+ border-start-start-radius: unset;
+ border-start-end-radius: unset;
+ border-end-start-radius: inherit;
+ border-end-end-radius: inherit;
+}
+
+.card:focus-visible {
+ outline: 2px solid currentColor;
+ outline-offset: 2px;
+}
+
+.card.bordered {
+ border-width: 1px;
+ --tw-border-opacity: 1;
+ border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));
+}
+
+.card.compact .card-body {
+ padding: 1rem;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+}
+
+.card.image-full :where(figure) {
+ overflow: hidden;
+ border-radius: inherit;
+}
+
.checkbox:focus {
box-shadow: none;
}
@@ -2320,6 +2420,10 @@ input.tab:checked + .tab-content,
--tw-placeholder-opacity: 0.2;
}
+.toast > * {
+ animation: toast-pop 0.25s ease-out;
+}
+
@keyframes toast-pop {
0% {
transform: scale(0.9);
@@ -2582,6 +2686,63 @@ input.tab:checked + .tab-content,
--tab-padding: 0.5rem;
}
+:where(.toast) {
+ bottom: 0px;
+ inset-inline-end: 0px;
+ inset-inline-start: auto;
+ top: auto;
+ --tw-translate-x: 0px;
+ --tw-translate-y: 0px;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.toast:where(.toast-start) {
+ inset-inline-end: auto;
+ inset-inline-start: 0px;
+ --tw-translate-x: 0px;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.toast:where(.toast-center) {
+ inset-inline-end: 50%;
+ inset-inline-start: 50%;
+ --tw-translate-x: -50%;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.toast:where(.toast-center):where([dir="rtl"], [dir="rtl"] *) {
+ --tw-translate-x: 50%;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.toast:where(.toast-end) {
+ inset-inline-end: 0px;
+ inset-inline-start: auto;
+ --tw-translate-x: 0px;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.toast:where(.toast-bottom) {
+ bottom: 0px;
+ top: auto;
+ --tw-translate-y: 0px;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.toast:where(.toast-middle) {
+ bottom: auto;
+ top: 50%;
+ --tw-translate-y: -50%;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.toast:where(.toast-top) {
+ bottom: auto;
+ top: 0px;
+ --tw-translate-y: 0px;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
.tooltip {
position: relative;
display: inline-block;
@@ -2746,6 +2907,10 @@ input.tab:checked + .tab-content,
bottom: 0px;
}
+.z-30 {
+ z-index: 30;
+}
+
.z-50 {
z-index: 50;
}
@@ -3149,16 +3314,31 @@ input.tab:checked + .tab-content,
background-color: rgb(246 246 246 / var(--tw-bg-opacity, 1));
}
+.bg-orange-400 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(251 146 60 / var(--tw-bg-opacity, 1));
+}
+
.bg-primary-100 {
--tw-bg-opacity: 1;
background-color: rgb(206 241 255 / var(--tw-bg-opacity, 1));
}
+.bg-primary-400 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(38 198 255 / var(--tw-bg-opacity, 1));
+}
+
.bg-primary-500 {
--tw-bg-opacity: 1;
background-color: rgb(0 159 255 / var(--tw-bg-opacity, 1));
}
+.bg-red-400 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(255 153 153 / var(--tw-bg-opacity, 1));
+}
+
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(255 119 119 / var(--tw-bg-opacity, 1));
@@ -3298,6 +3478,10 @@ input.tab:checked + .tab-content,
padding-right: 0.25rem;
}
+.pt-12 {
+ padding-top: 3rem;
+}
+
.pt-20 {
padding-top: 5rem;
}
@@ -3446,6 +3630,12 @@ input.tab:checked + .tab-content,
opacity: 0;
}
+.shadow-lg {
+ --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
+ --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
.shadow-sm {
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
@@ -3460,6 +3650,14 @@ input.tab:checked + .tab-content,
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
+.transition {
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
.transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
diff --git a/ui/builder/builder.templ b/ui/builder/builder.templ
index 29cbb1d..c731dac 100644
--- a/ui/builder/builder.templ
+++ b/ui/builder/builder.templ
@@ -74,7 +74,7 @@ templ FormBuilderNavTitle(form frm.Form) {
// FormBuilderNav is the top-of-the-page navigation bar
templ FormBuilderNav(form frm.Form) {
- ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -685,33 +795,33 @@ func FieldSet(args FieldsetArgs) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var31 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var31 == nil {
- templ_7745c5c3_Var31 = templ.NopComponent
+ templ_7745c5c3_Var35 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var35 == nil {
+ templ_7745c5c3_Var35 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -736,29 +846,29 @@ func Head(pageTitle string) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var33 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var33 == nil {
- templ_7745c5c3_Var33 = templ.NopComponent
+ templ_7745c5c3_Var37 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var37 == nil {
+ templ_7745c5c3_Var37 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "
+