feat: preferences, exclusion-aware planner, /onboarding (PRD §5.1, §3)
Preferences (PRD §5.1) - @ketopath/shared: zod schemas + EXCLUSION_GROUPS, CUISINE_TAGS, COOKING_TIMES, FASTING_PROTOCOL_VALUES - apps/api: GET/PATCH /me/preferences (exclusions, cuisinePreferences, cookingTime, fastingProtocol) - apps/web: PreferencesForm chip-based + PreferencesSection wired into /profile so existing users can edit - Plan generation now aggregates Ingredient.exclusionGroups → Recipe exclusionTags so matchMeals filters out excluded recipes Onboarding (PRD §3) - /onboarding multi-step (profile → preferences → done) with smart-jump: starts at the first incomplete step - ProfileForm gains optional onSaved/saveLabel for reuse outside /profile - Redirect chain updated: /welcome accept → /onboarding (was /profile); /plan, /tracking, /shopping redirect missing profiles to /onboarding - /profile remains the editing surface for returning users i18n - New Onboarding + Preferences namespaces (it) Smoke tested end-to-end: selected fish exclusion, regenerated the plan, verified Salmone/Branzino/Tonno are filtered out and unaffected recipes (Frittata, Pollo, Mandorle, Polpette) remain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
cf7361f735
commit
624152b29b
18 files changed
+864
-22
No files matched your search
@@ -0,0 +1,51 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// PRD §5.1 — gruppi di esclusione legati agli ingredienti.
|
||||
// Devono coincidere con i tag su `Ingredient.exclusionGroups` nel seed
|
||||
// (vedi packages/db/prisma/seed-ingredients.ts).
|
||||
export const EXCLUSION_GROUPS = [
|
||||
'lactose',
|
||||
'egg',
|
||||
'fish',
|
||||
'nuts',
|
||||
'gluten',
|
||||
'pork',
|
||||
'beef',
|
||||
] as const;
|
||||
export type ExclusionGroup = (typeof EXCLUSION_GROUPS)[number];
|
||||
|
||||
// Tradizioni regionali italiane + low-carb internazionali.
|
||||
// Per ora informative: il matchmaking ne tiene conto solo come bonus.
|
||||
export const CUISINE_TAGS = [
|
||||
'mediterranea',
|
||||
'italiana',
|
||||
'pugliese',
|
||||
'siciliana',
|
||||
'toscana',
|
||||
'medio-orientale',
|
||||
'nordica',
|
||||
] as const;
|
||||
export type CuisineTag = (typeof CUISINE_TAGS)[number];
|
||||
|
||||
export const COOKING_TIMES = ['LOW', 'MEDIUM', 'HIGH'] as const;
|
||||
export type CookingTimeLevel = (typeof COOKING_TIMES)[number];
|
||||
|
||||
export const FASTING_PROTOCOL_VALUES = [
|
||||
'FOURTEEN_TEN',
|
||||
'SIXTEEN_EIGHT',
|
||||
'EIGHTEEN_SIX',
|
||||
'TWENTY_FOUR',
|
||||
'ESE_24',
|
||||
'FIVE_TWO',
|
||||
] as const;
|
||||
|
||||
export const preferencesPatchSchema = z
|
||||
.object({
|
||||
exclusions: z.array(z.enum(EXCLUSION_GROUPS)).max(EXCLUSION_GROUPS.length).optional(),
|
||||
cuisinePreferences: z.array(z.enum(CUISINE_TAGS)).max(CUISINE_TAGS.length).optional(),
|
||||
cookingTime: z.enum(COOKING_TIMES).optional(),
|
||||
fastingProtocol: z.enum(FASTING_PROTOCOL_VALUES).nullable().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type PreferencesPatch = z.infer<typeof preferencesPatchSchema>;
|
||||
Reference in new issue
Block a user