src/features/nutrition. It renders the coach’s nutrition plan, lets the trainee tick, swap, resize and remove plan foods, add their own meals, and log water. Everything the trainee changes for a day is kept in one Zustand store and synced to the server as a day snapshot.
Related pages:
Folder map
Note the two store folders.
store/ (singular) holds the day log. stores/ (plural) holds only the meal draft.
Routes
All route files are thin wrappers, exceptmeal/add.tsx and meal/result.tsx, which contain their screens inline.
Plan structure
getNutritionPlan() calls GET /v1/trainee/nutrition and returns an array of plans. A trainee can have several nutrition plans at once.
nutritionApi.ts:
kindcomes fromplanKindOf(plan)insrc/features/plans/lib/planKind.ts. The server’skindwins when present. An older API that only sendspdfUrlstill produces a file plan.autoAltsistrueonly when the server sends exactlytrue.- Per-day targets:
day.targets ?? plan.targets. A day without its own targets inherits the plan’s. - A meal’s
labelis the coach’s custom name, else a label looked up bytype(breakfast,snack_am,lunch,pre,post,dinner), else a generic meal label. - Each item’s
options[0]is the coach’s main food. The rest are alternatives mapped byaltOptionOf. - An option’s
qtyTextis the server’sdisplay.textwhen present, elseqtyplus a unit label. meal.targetis only set when the server sends one.
NutritionTargetsDto carries kcal, protein, carbs, fat, water and optional mbpProtein, mbpCarb, mbpFat.
Multiple plans, days and file plans
PlanScreen shows two pickers:
PlanTitlePicker(components/PlanTitlePicker.tsx) switches between plans. It is a plain label when there is one plan and a modal list when there are more. A file plan shows a badge.PlanSwitcher(src/components/ui/PlanSwitcher.tsx) is a horizontal chip row. On the nutrition tab it is fed the selected plan’s days, not plans, and renders only when the plan has more than one day.
resolveNutritionPlanPick and resolveNutritionDayPick in lib/day.ts, in this order:
- The trainee’s explicit pick for this date (
planPickanddayPicklocal state, keyed by date). - For today only, the persisted preferred day while it is still active.
- The plan day the day’s edits reference most, from
referencedDayOfindata/calc.ts. - The first plan and the first day.
File and link plans
src/features/plans/lib/planKind.ts decides what a file plan is and how to show it.
FilePlanView (src/features/plans/components/FilePlanView.tsx) renders the file in a WebView. It has an expand button that opens the shared ContentPdfViewer full screen, and one action button: a link opens in the browser, a PDF is downloaded to the cache directory and passed to the share sheet, falling back to opening the url. FilePlanNotice is the passive card shown where macro progress would normally go.
A file plan has no days or meals. When a file plan is selected on an editable day, PlanScreen turns its own scroll off (a WebView needs a bounded height) and shows the water tile, the notice, the file, any meals the trainee added by hand, and the add buttons.
Screens
PlanScreen
presentation/PlanScreen.tsx is the tab. It calls useNutritionDaySync() once and reads the whole session store into a DayEdits object. The body depends on the selected date:
An editable day is today or yesterday.
DateNavigator disables future dates in its calendar.
The macro header is built in the macros memo. Goals are day.targets when the coach set them, else the sum of the day’s planned meals (plannedDayMacros). Consumed is eatenDayMacros. When the studio uses MBP, the four rows show MBP totals instead of kcal and grams.
The two add buttons go to /meal/add with mode: 'photo' and to /meal/new with the current plan and day ids.
Deleting a meal asks for confirmation with Alert.alert. A trainee meal that has a logId also deletes that meal log through useDeleteMealLog. A plan meal is reset to PLANNED and then marked removed for the day.
MealScreen
presentation/MealScreen.tsx finds the meal by id across every plan and day (findMeal). It shows:
MealMacroSummarywith the meal total and what was eaten.MealTargetGaugewhen the meal has a coach limit.AiComponentRowfor foods the trainee added to this meal.FoodItemRowfor each plan item that is not removed, with the tick, the swap chip, the amount editor and swipe to delete.- A footer with “mark all eaten” and an AI replace button that opens
MealAiLoggerin a modal.
AddComponentSheet automatically after AUTO_OPEN_DELAY_MS (350 ms), once per visit.
On a future date the screen shows a lock banner, disables the list and hides the footer.
MealComposerScreen
presentation/MealComposerScreen.tsx builds a trainee’s own meal (CustomMeal). Foods come from AddComponentSheet. Each row has a PortionStepper when the food carries portion context, and three editable macro fields. The header shows “remaining today”: the day goal minus what is already eaten minus the draft. Saving calls upsertCustomMeal(dateKey, meal) and goes back. When editing, the original meal is captured once on mount so a store echo cannot reset the draft.
Endpoints used by the data layer
All paths are relative to the API base and go throughapiClient (see API client).
Water uses
/v1/trainee/water and is documented in Water.
Query keys
Defined inhooks/useNutrition.ts.
useSaveNutritionDay invalidates ['home'] on success, because the home rings read the same day log. useLogQuickMeal and useDeleteMealLog invalidate the menu, the history and ['home'].
Tests
Four Node test scripts inscripts/ load the pure modules with a small TypeScript transpile loader and assert the intended behaviour: test-nutrition-portions.cjs, test-meal-target.cjs, test-added-food-amount.cjs and test-plan-kind.cjs. They use node:test. No package.json script references them by name. See Testing and lint.