feat: profile flow — Zod schema, /me/profile API, /profile page with BMR/TDEE
@ketopath/shared — new module profile/schema:
- profileInputSchema (Zod): age 18-110, gender, height 120-230 cm, weights
35-300 kg, activityLevel, optional targetDate
- GENDERS / ACTIVITY_LEVELS string-literal arrays for UI iteration
- Re-exported from @ketopath/shared
apps/api — new module modules/profile:
- PUT /me/profile: requireAuth, validates body via profileInputSchema, upserts
the row, returns the saved profile + derived { bmr, tdee, activityMultiplier }
(BMR/TDEE computed via @ketopath/shared)
- GET /me/profile: requireAuth, returns the same shape, 404 when missing
- Decimal columns serialised back as numbers
apps/web — new /profile page:
- src/app/[locale]/profile/page.tsx (server): redirects unauthenticated users
to /sign-in, calls fetchProfile to hydrate the form
- profile-form.tsx (client): react-hook-form + zodResolver bound to the same
shared schema, native styled selects for gender/activityLevel until shadcn
Select arrives, post-submit panel showing BMR/TDEE/multiplier
- actions.ts: server actions saveProfile / fetchProfile that proxy the call
to API_URL via cookie passthrough (no CORS, no exposed token)
- Home page gains a "Completa il tuo profilo" CTA when signed in
- API_URL env var added to .env.example
- Italian copy in messages/it.json under Profile
Verified end-to-end with the "Michele" PRD persona (49, M, 170cm, 76kg, SEDENTARY):
BMR = 1583 kcal, TDEE = 1899 kcal, multiplier 1.2 — matches the unit tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
ac2227fdec
commit
1d904484e6
12 files changed
+398
-1
No files matched your search
@@ -1,3 +1,4 @@
|
||||
export * from './nutrition/bmr.js';
|
||||
export * from './nutrition/tdee.js';
|
||||
export * from './nutrition/types.js';
|
||||
export * from './profile/schema.js';
|
||||
@@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const GENDERS = ['MALE', 'FEMALE', 'OTHER'] as const;
|
||||
export const ACTIVITY_LEVELS = ['SEDENTARY', 'LIGHT', 'MODERATE', 'INTENSE'] as const;
|
||||
|
||||
export const profileInputSchema = z.object({
|
||||
age: z.coerce.number().int().min(18, 'Devi avere almeno 18 anni').max(110),
|
||||
gender: z.enum(GENDERS),
|
||||
heightCm: z.coerce.number().int().min(120).max(230),
|
||||
weightStartKg: z.coerce.number().min(35).max(300),
|
||||
weightCurrentKg: z.coerce.number().min(35).max(300),
|
||||
weightGoalKg: z.coerce.number().min(35).max(300),
|
||||
activityLevel: z.enum(ACTIVITY_LEVELS),
|
||||
targetDate: z.coerce.date().optional(),
|
||||
});
|
||||
|
||||
export type ProfileInput = z.input<typeof profileInputSchema>;
|
||||
export type ProfileInputParsed = z.output<typeof profileInputSchema>;
|
||||
Reference in new issue
Block a user