- 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.
99 lines
2.7 KiB
TypeScript
99 lines
2.7 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { Button } from '@/components/ui/button'
|
|
import { FormRenderer } from '@/components/forms/form-renderer'
|
|
import { submitForm } from '@/lib/actions'
|
|
import type { FormField } from '@/lib/forms-schema'
|
|
|
|
// Authenticated member fill surface for members-only / in-app forms. The
|
|
// member is already signed in, so submitForm's visibility check passes;
|
|
// submission is by formId (slugs are per-space now).
|
|
export function MemberFillClient({
|
|
formId,
|
|
description,
|
|
kind,
|
|
legalText,
|
|
fields,
|
|
}: {
|
|
formId: string
|
|
description: string | null
|
|
kind: string
|
|
legalText: string | null
|
|
fields: FormField[]
|
|
}) {
|
|
const router = useRouter()
|
|
const [values, setValues] = useState<Record<string, unknown>>({})
|
|
const [consent, setConsent] = useState(false)
|
|
const [submitting, setSubmitting] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [done, setDone] = useState(false)
|
|
|
|
const isWaiver = kind === 'waiver'
|
|
|
|
async function submit() {
|
|
setError(null)
|
|
setSubmitting(true)
|
|
const res = await submitForm({
|
|
formId,
|
|
answers: values,
|
|
consent: isWaiver ? consent : undefined,
|
|
})
|
|
setSubmitting(false)
|
|
if ('error' in res && res.error) {
|
|
setError(res.error)
|
|
return
|
|
}
|
|
setDone(true)
|
|
}
|
|
|
|
if (done) {
|
|
return (
|
|
<div className="space-y-4 rounded-lg border p-8 text-center">
|
|
<h2 className="text-xl font-semibold">Thank you</h2>
|
|
<p className="text-sm text-muted-foreground">Your response has been recorded.</p>
|
|
<Button variant="secondary" onClick={() => router.push('/my-forms')}>
|
|
Back to forms
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{description && <p className="text-muted-foreground">{description}</p>}
|
|
|
|
{isWaiver && legalText && (
|
|
<div className="max-h-72 overflow-y-auto whitespace-pre-wrap rounded-lg border bg-muted/30 p-4 text-sm">
|
|
{legalText}
|
|
</div>
|
|
)}
|
|
|
|
<FormRenderer
|
|
fields={fields}
|
|
values={values}
|
|
onChange={(k, v) => setValues(p => ({ ...p, [k]: v }))}
|
|
/>
|
|
|
|
{isWaiver && (
|
|
<label className="flex items-start gap-2 text-sm">
|
|
<input
|
|
type="checkbox"
|
|
checked={consent}
|
|
onChange={e => setConsent(e.target.checked)}
|
|
className="mt-1"
|
|
/>
|
|
I have read and agree to the terms above.
|
|
</label>
|
|
)}
|
|
|
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
|
|
<Button onClick={submit} disabled={submitting}>
|
|
{submitting ? 'Submitting…' : 'Submit'}
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|