From 7476eabec669740f044825812096679359b7ec79 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 25 May 2026 13:26:04 +0200 Subject: [PATCH] feat(form-error-ux): adopt useFormScrollToError + FormErrorSummary across remaining 10 forms Completes the form-error rollout the prior session shipped on the 6 highest-impact forms (client/interest/yacht/company/berth/expense). Adds the scroll-to-first-error wrapper + the top-of-form summary banner to: - src/app/(auth)/login/page.tsx - src/app/(auth)/reset-password/page.tsx - src/app/(auth)/set-password/page.tsx - src/app/(auth)/setup/page.tsx - src/app/(dashboard)/[portSlug]/invoices/new/page.tsx - src/components/berths/berth-detail-header.tsx (status-change dialog) - src/components/companies/add-membership-dialog.tsx - src/components/invoices/invoice-detail.tsx (record-payment form) - src/components/reservations/berth-reserve-dialog.tsx - src/components/yachts/yacht-transfer-dialog.tsx Each call site: hook wraps handleSubmit, FormErrorSummary renders only when 2+ errors fire (no visual change otherwise), and per-form `labels` prop translates field names to human-readable strings. invoice-line-items is a sub-form via useFormContext, so it inherits from the parent. 1471/1471 vitest, tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/(auth)/login/page.tsx | 9 ++++++++- src/app/(auth)/reset-password/page.tsx | 6 +++++- src/app/(auth)/set-password/page.tsx | 9 ++++++++- src/app/(auth)/setup/page.tsx | 14 +++++++++++++- .../(dashboard)/[portSlug]/invoices/new/page.tsx | 16 +++++++++++++++- src/components/berths/berth-detail-header.tsx | 11 +++++++++-- .../companies/add-membership-dialog.tsx | 9 ++++++++- src/components/invoices/invoice-detail.tsx | 12 +++++++++++- .../reservations/berth-reserve-dialog.tsx | 14 ++++++++++++-- src/components/yachts/yacht-transfer-dialog.tsx | 13 ++++++++++++- 10 files changed, 101 insertions(+), 12 deletions(-) diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 751c0387..3b6e86a3 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -13,6 +13,8 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { BrandedAuthShell } from '@/components/shared/branded-auth-shell'; import { useAuthBranding } from '@/components/shared/auth-branding-provider'; +import { FormErrorSummary } from '@/components/forms/form-error-summary'; +import { useFormScrollToError } from '@/hooks/use-form-scroll-to-error'; // `identifier` accepts either an email address or a username (3–30 lowercase // letters / digits / dot / underscore / hyphen). The server endpoint @@ -75,6 +77,7 @@ export default function LoginPage() { } = useForm({ resolver: zodResolver(loginSchema), }); + const submitWithScroll = useFormScrollToError(handleSubmit, errors); async function onSubmit(data: LoginFormData) { setIsLoading(true); @@ -112,7 +115,11 @@ export default function LoginPage() {

Sign in to continue

-
+ +
({ resolver: zodResolver(resetSchema), }); + const submitWithScroll = useFormScrollToError(handleSubmit, errors); // If the user landed here from a stale email link that points to // `/reset-password?token=…` instead of `/set-password?token=…`, hand @@ -96,7 +99,8 @@ export default function ResetPasswordPage() {
) : ( - + +
({ resolver: zodResolver(passwordSchema), }); + const submitWithScroll = useFormScrollToError(handleSubmit, errors); async function onSubmit(data: SetPasswordFormData) { if (!token) { @@ -138,7 +141,11 @@ function SetPasswordInner() {

Choose a password for your CRM account

- + +
({ resolver: zodResolver(setupSchema), }); + const submitWithScroll = useFormScrollToError(handleSubmit, errors); useEffect(() => { let cancelled = false; @@ -119,7 +122,16 @@ export default function SetupPage() {

- + +
- + + {/* Step 1: Client Info */} {step === 1 && ( diff --git a/src/components/berths/berth-detail-header.tsx b/src/components/berths/berth-detail-header.tsx index d4c7066d..46d2a285 100644 --- a/src/components/berths/berth-detail-header.tsx +++ b/src/components/berths/berth-detail-header.tsx @@ -25,6 +25,8 @@ import { } from '@/components/ui/select'; import { Textarea } from '@/components/ui/textarea'; import { DetailHeaderStrip } from '@/components/shared/detail-header-strip'; +import { FormErrorSummary } from '@/components/forms/form-error-summary'; +import { useFormScrollToError } from '@/hooks/use-form-scroll-to-error'; import { PermissionGate } from '@/components/shared/permission-gate'; import { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill'; import { BerthForm } from './berth-form'; @@ -128,11 +130,12 @@ function StatusChangeDialog({ setValue, watch, reset, - formState: { isSubmitting }, + formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(updateBerthStatusSchema), defaultValues: { status: currentStatus as (typeof BERTH_STATUSES)[number], reason: '' }, }); + const submitWithScroll = useFormScrollToError(handleSubmit, errors); const status = watch('status'); const interestId = watch('interestId'); @@ -180,7 +183,11 @@ function StatusChangeDialog({ Change Status - + +