087fbdd176
Budget.Core: entities, DTOs, enums, FrequencyCalculator (no EF/ASP.NET deps) Budget.Infrastructure: AppDbContext, migrations, BudgetAuthorizationService Budget.Api: controllers, middleware, Program.cs — references both projects EF and Npgsql packages moved to Infrastructure; Api retains only JwtBearer, HealthChecks, and EF.Design (needed for dotnet ef CLI). Dockerfile updated to copy all three project directories before publishing. Migration namespaces updated from Budget.Api.Data.* to Budget.Infrastructure.Data.* and model type strings updated to Budget.Core.Models.* in the snapshot. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.3 KiB
4.3 KiB
Plan: Add react-hook-form + zod for Form Validation
Goal
Replace ad-hoc form state (useState per field, manual validation) with react-hook-form
and zod schemas, matching the stwaddle stack convention. Validation errors render inline
next to fields.
Prerequisite: Plan #12 (TanStack React Query) must be complete. Mutation hooks from
that plan are used as the submit handlers here — useForm handleSubmit calls
mutation.mutateAsync().
Current state
Forms are managed ad-hoc:
const [newName, setNewName] = useState('');
const handleCreate = async () => {
if (!newName.trim()) return;
await createMutation.mutateAsync({ name: newName.trim() });
};
No schema validation, no field-level error messages, no loading/disabled states during submit.
Steps
Phase 1 — Install
npm install react-hook-form zod @hookform/resolversinsrc/Budget.Client/.
Phase 2 — Create zod schemas
- Create
src/Budget.Client/src/schemas/index.tswith one schema per create/update request:
export const createBudgetSchema = z.object({
name: z.string().min(1, 'Name is required').max(200),
});
export const createIncomeSchema = z.object({
name: z.string().min(1, 'Name is required').max(200),
frequency: z.nativeEnum(Frequency),
amount: z.number({ invalid_type_error: 'Amount is required' }).positive('Must be positive'),
});
export const createOutgoSchema = z.object({
name: z.string().min(1).max(200),
category: z.string().max(100).optional(),
type: z.nativeEnum(OutgoType),
frequency: z.nativeEnum(Frequency),
amount: z.number().positive(),
paymentSource: z.string().max(100).optional(),
notes: z.string().max(1000).optional(),
});
export const createShareSchema = z.object({
email: z.string().email('Must be a valid email'),
permission: z.nativeEnum(SharePermission),
});
export const updateTaxRateSchema = z.object({
effectiveTaxRate: z.number().min(0).max(0.99),
});
Phase 3 — Update forms in pages
For each form, the pattern is:
const { register, handleSubmit, reset, formState: { errors, isSubmitting } } = useForm<CreateBudgetRequest>({
resolver: zodResolver(createBudgetSchema),
});
const onSubmit = async (data: CreateBudgetRequest) => {
await createBudget.mutateAsync(data);
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
<button type="submit" disabled={isSubmitting}>Create</button>
</form>
);
BudgetsPage.tsx— wirecreateBudgetSchemainto the budget name input.IncomePage.tsx— wirecreateIncomeSchema/updateIncomeSchemafor the add and edit income forms.OutgoPage.tsx— wirecreateOutgoSchema/updateOutgoSchemafor add and edit outgo forms (this is the most complex form).SettingsPage.tsx— wirecreateShareSchemafor the invite form;updateShareSchemafor the permission dropdown.SummaryPage.tsx— wireupdateTaxRateSchemafor the tax rate field.
Phase 4 — Disable submit during mutation
- Set
disabled={isSubmitting || mutation.isPending}on submit buttons. This prevents double-submits while the network request is in flight.
Phase 5 — Build and verify
npm run build— zero TypeScript errors.- Manually verify that submitting an empty name shows an inline error without hitting the API.
Key decisions
- Schemas live in a single
schemas/index.tsfor now — split into per-domain files if they grow large. amountfields: HTML<input type="number">returns a string; usez.coerce.number()orvalueAsNumber: trueinregister()to get a proper number before Zod validates.- Update schemas (for edit forms) reuse the same shape as create schemas where the fields are identical — they are not duplicated.
- No server-side error mapping in this plan: if the API returns 422/400 with
{ error: "..." }, the global MutationCache toast (from plan #12) handles it. Field-level server errors can be mapped viasetErrorin a future pass if needed.
Files affected
package.json- New:
src/Budget.Client/src/schemas/index.ts src/Budget.Client/src/pages/BudgetsPage.tsxsrc/Budget.Client/src/pages/IncomePage.tsxsrc/Budget.Client/src/pages/OutgoPage.tsxsrc/Budget.Client/src/pages/SettingsPage.tsxsrc/Budget.Client/src/pages/SummaryPage.tsx