diff --git a/backend/templates/layout.templ b/backend/templates/layout.templ index 1238314..7fb9941 100644 --- a/backend/templates/layout.templ +++ b/backend/templates/layout.templ @@ -49,7 +49,7 @@ templ Layout(title string, user *auth.User, csrfToken string) { { children... } diff --git a/backend/templates/tablos.templ b/backend/templates/tablos.templ new file mode 100644 index 0000000..491bf23 --- /dev/null +++ b/backend/templates/tablos.templ @@ -0,0 +1,179 @@ +package templates + +import ( + "backend/internal/auth" + "backend/internal/db/sqlc" + "backend/internal/web/ui" +) + +// TablosDashboard renders the root authenticated dashboard: heading, "New tablo" +// button, create-form slot, and the list of tablo cards (or empty state). +// UI-SPEC §1 Interaction Contract — GET /. +templ TablosDashboard(user *auth.User, csrfToken string, tablos []sqlc.Tablo) { + @Layout("Tablos — Xtablo", user, csrfToken) { +
+

Your Tablos

+ @ui.Button(ui.ButtonProps{ + Label: "New tablo", + Variant: ui.ButtonVariantDefault, + Tone: ui.ButtonToneSolid, + Size: ui.SizeMD, + Type: "button", + Attrs: templ.Attributes{ + "hx-get": "/tablos/new", + "hx-target": "#create-form-slot", + "hx-swap": "innerHTML", + }, + }) +
+
+
+ if len(tablos) == 0 { + @TablosEmptyState() + } else { + for _, tablo := range tablos { + @TabloCard(tablo, csrfToken) + } + } +
+ } +} + +// TablosEmptyState renders the empty-state copy when a user has no tablos. +// Copy strings are locked by UI-SPEC Copywriting Contract. +templ TablosEmptyState() { +
+

No tablos yet

+

Create your first tablo to get started.

+
+ @ui.Button(ui.ButtonProps{ + Label: "New tablo", + Variant: ui.ButtonVariantDefault, + Tone: ui.ButtonToneSolid, + Size: ui.SizeMD, + Type: "button", + Attrs: templ.Attributes{ + "hx-get": "/tablos/new", + "hx-target": "#create-form-slot", + "hx-swap": "innerHTML", + "aria-label": "Create your first tablo", + }, + }) +
+
+} + +// TabloCard renders a single tablo as a ui.Card on the dashboard. +// Guards description and color rendering against null pgtype.Text values (Pitfall 6). +// Wraps the Delete button in a .tablo-delete-zone div for Plan 03's delete-confirm swap. +templ TabloCard(tablo sqlc.Tablo, csrfToken string) { + @ui.Card(templ.Attributes{"id": "tablo-" + tablo.ID.String()}) { +
+
+

{ tablo.Title }

+ if tablo.Description.Valid && tablo.Description.String != "" { +

{ tablo.Description.String }

+ } + if tablo.Color.Valid && tablo.Color.String != "" { +
+ + { tablo.Color.String } +
+ } +
+
+ @ui.Button(ui.ButtonProps{ + Label: "Delete", + Variant: ui.ButtonVariantDanger, + Tone: ui.ButtonToneSoft, + Size: ui.SizeMD, + Type: "button", + Attrs: templ.Attributes{ + "hx-get": "/tablos/" + tablo.ID.String() + "/delete-confirm", + "hx-target": "closest .tablo-delete-zone", + "hx-swap": "outerHTML", + }, + }) +
+
+
+ View +
+ } +} + +// TabloCreateFormFragment renders the inline create form loaded into #create-form-slot +// via HTMX. Falls back to a plain POST /tablos for non-JS paths. +// UI-SPEC §2 Interaction Contract — GET /tablos/new + POST /tablos. +templ TabloCreateFormFragment(form TabloCreateForm, errs TabloCreateErrors, csrfToken string) { +
+ @ui.CSRFField(csrfToken) + @GeneralError(errs.General) +

Create a tablo

+
+ + + @FieldError(errs.Title) +
+
+ + +
+
+ + +
+
+ @ui.Button(ui.ButtonProps{ + Label: "Create tablo", + Variant: ui.ButtonVariantDefault, + Tone: ui.ButtonToneSolid, + Size: ui.SizeMD, + Type: "submit", + }) + Cancel +
+
+} + +// TabloCardWithOOBFormClear renders a TabloCard as the primary swap target +// AND an OOB element that clears #create-form-slot in the same response. +// The OOB div MUST be a top-level sibling of TabloCard — NOT nested (Pitfall 5). +// HTMX applies the primary swap (HX-Retarget: #tablos-list, afterbegin) AND +// the OOB swap (#create-form-slot → empty) from a single response. +templ TabloCardWithOOBFormClear(tablo sqlc.Tablo, csrfToken string) { + @TabloCard(tablo, csrfToken) +
+} diff --git a/backend/templates/tablos_forms.go b/backend/templates/tablos_forms.go new file mode 100644 index 0000000..80ab8b5 --- /dev/null +++ b/backend/templates/tablos_forms.go @@ -0,0 +1,16 @@ +package templates + +// TabloCreateForm carries the submitted field values back to the template so +// inputs can be repopulated on validation failure. +type TabloCreateForm struct { + Title string + Description string + Color string +} + +// TabloCreateErrors holds per-field and general error messages for the tablo +// create form. A field with an empty string means "no error for this field". +type TabloCreateErrors struct { + Title string + General string +}