feat(forms): obvious publish flow, member Forms surface, templates (P8)

- builder: draft/published/closed status banners; creating a form now
  lands on its edit page so Publish is immediately visible; a
  members-only form shows a "no public link" note instead of a dead URL
- member-facing forms: /my-forms list + /my-forms/[id] fill
  (MemberFillClient), "Forms" sidebar item for all members. Fixes the
  long-standing gap where members-only forms had no fill surface
- starter templates: lib/form-templates.ts + a picker on /forms/new
  (Liability waiver, Membership application, Event RSVP, Photo release,
  Blank) that seeds builder state (no table)
- forms-client relabels members-only forms (was showing a misleading
  /f/<space>/<slug> that always 404s for members-only)
- docs corrected per the audit pass: DATABASE_SCHEMA / DB_SCHEMA_MAP /
  ARCHITECTURE / API_REFERENCE now reflect migrations 014-029, per-space
  slug, /f/[space]/[slug], getPublicForm({space,slug}), invite roles;
  HANDOFF pass-19 added with residual gaps

337 tests; build verified (Compiled successfully, exit 0). No deploy.
This commit is contained in:
Moheeb Zara
2026-05-16 17:41:16 -07:00
parent df6951c323
commit 73eef7293a
14 changed files with 485 additions and 33 deletions
+37 -2
View File
@@ -135,8 +135,15 @@ export function FormBuilder({ initial }: { initial: BuilderForm }) {
toast.error(res.error || 'Could not save')
return
}
toast.success(isEdit ? 'Form saved' : 'Form created')
router.push('/forms')
toast.success(isEdit ? 'Form saved' : 'Form created — now publish it to make it live')
if (isEdit) {
router.push('/forms')
} else {
// Land on the new form's edit page so the Publish step is right there
// (creating a form leaves it as a draft).
const newId = 'data' in res && res.data ? res.data.id : undefined
router.push(newId ? `/forms/${newId}/edit` : '/forms')
}
router.refresh()
}
@@ -180,6 +187,28 @@ export function FormBuilder({ initial }: { initial: BuilderForm }) {
<div className="grid gap-6 lg:grid-cols-2">
{/* Editor */}
<div className="space-y-6">
{isEdit && initial.status === 'draft' && (
<div className="rounded-lg border border-amber-500/40 bg-amber-500/10 p-3 text-sm">
<strong>Draft.</strong> This form is not visible to anyone yet. Use{' '}
<strong>Publish</strong> below to make it live.
</div>
)}
{isEdit && initial.status === 'published' && (
<div className="rounded-lg border border-emerald-500/40 bg-emerald-500/10 p-3 text-sm">
<strong>Published.</strong> This form is live and accepting responses.
</div>
)}
{isEdit && initial.status === 'closed' && (
<div className="rounded-lg border bg-muted/40 p-3 text-sm">
<strong>Closed.</strong> No longer accepting responses. Re-publish to reopen.
</div>
)}
{!isEdit && (
<div className="rounded-lg border bg-muted/40 p-3 text-sm text-muted-foreground">
Build your form, then <strong>Create</strong>. It starts as a draft; you publish it on
the next screen.
</div>
)}
{isEdit && (
<div className="flex flex-wrap items-center gap-2 rounded-lg border p-3">
<Badge variant={initial.status === 'published' ? 'default' : 'secondary'}>
@@ -221,6 +250,12 @@ export function FormBuilder({ initial }: { initial: BuilderForm }) {
</Button>
</div>
)}
{isEdit && initial.status === 'published' && visibility === 'members' && (
<div className="rounded-lg border bg-muted/40 p-3 text-sm text-muted-foreground">
Members-only: there is no public link. Members fill this from their{' '}
<strong>Forms</strong> page in the app.
</div>
)}
<div className="space-y-3">
<SectionTitle>Form details</SectionTitle>