Skip to content

feat(nutrition): filter ingredient search by Nutri-Score#1237

Merged
rolandgeider merged 2 commits intowger-project:masterfrom
kiranannadatha8:feature/2295-nutriscore-filter
Apr 20, 2026
Merged

feat(nutrition): filter ingredient search by Nutri-Score#1237
rolandgeider merged 2 commits intowger-project:masterfrom
kiranannadatha8:feature/2295-nutriscore-filter

Conversation

@kiranannadatha8
Copy link
Copy Markdown
Contributor

@kiranannadatha8 kiranannadatha8 commented Apr 19, 2026

Summary

Adds a Nutri-Score filter to the ingredient autocompleter, as requested in wger-project/wger#2295. Users can now narrow search results to ingredients graded at or better than a chosen Nutri-Score (A–E).

When the filter is enabled, the selected "worst acceptable grade" is sent to the backend as nutriscore__lte=<grade>. The toggle state and grade are persisted in localStorage, matching the existing vegan / vegetarian / language filter pattern.

Changes

  • src/services/ingredient.ts — refactored searchIngredient from five positional parameters to (name, filters: IngredientSearchFilters). This keeps future optional filters from shifting call sites. Adds nutriscoreMax support.
  • src/components/Nutrition/widgets/IngredientAutcompleter.tsx — new Switch + MUI Slider (A/B/C/D/E marks) inside the filter popover. Defaults to C; hidden until the switch is enabled; persisted to localStorage.
  • src/components/Nutrition/widgets/IngredientAutocompleter.test.tsx — 5 new tests covering slider visibility, localStorage round-trip, and the resulting searchIngredient call shape. Added localStorage.clear() to beforeEach to prevent cross-test leakage.
  • public/locales/en/translation.json — two new i18n keys: nutrition.filterNutriscore, nutrition.filterNutriscoreMax.

UI

Inside the existing filter popover (revealed by the tune icon next to the search input), below the Vegan / Vegetarian switches:

nutriscore-filter-B

The slider is hidden until the "Filter by Nutri-Score" switch is turned on. Screenshots are available on request (or reproducible by mounting IngredientSearch against a local dev server).

Depends on

Test plan

  • jest src/components/Nutrition/widgets/IngredientAutocompleter.test.tsx → 13/13 pass
  • jest src/services → all pass
  • Manual: toggle the Nutri-Score switch, confirm slider appears; move slider, reload, confirm state restored from localStorage
  • Manual: type an ingredient query, confirm nutriscore__lte appears in the outgoing request when the filter is on and is omitted when off
  • Reviewer: verify on a staging instance pointing at a backend with #2305 merged

Related

Add a Nutri-Score filter to the ingredient autocompleter. When enabled,
a Material-UI slider lets the user pick the worst acceptable grade
(A–E); the value is sent to the backend as `nutriscore__lte` so that
only ingredients at or better than that grade are returned.

* refactor `searchIngredient` to accept an `IngredientSearchFilters`
  object instead of five positional parameters — this makes it safe
  to add further optional filters without shifting call sites
* persist the toggle state and selected grade in localStorage using
  the same pattern as the existing vegan / vegetarian / language
  filters
* add 5 tests covering slider visibility, localStorage round-trip,
  and the resulting API call shape; clear localStorage in beforeEach
  to prevent leakage between tests

Depends on wger-project/wger PR #2305 which adds the
`nutriscore__lte` lookup to the backend filterset.

Refs #2295
@coveralls
Copy link
Copy Markdown
Collaborator

coveralls commented Apr 19, 2026

Coverage Status

coverage: 74.8% (+0.08%) from 74.716% — kiranannadatha8:feature/2295-nutriscore-filter into wger-project:master

@rolandgeider
Copy link
Copy Markdown
Member

I'm thinking if we could remove the toggle completely by adding an option to the slider (something like "off" or "no filter") that turned off the search

Addresses review feedback on wger-project#1237 — the Switch + conditional Slider
combination expressed one semantic decision ("what grade ceiling?")
through two controls. Folded "Off" into the slider itself:

- Slider stops are now Off | A | B | C | D | E (was A..E, gated by Switch).
- Off position sends no `nutriscore__lte`; A..E map as before.
- Heading changed from "Worst acceptable Nutri-Score" to "Nutri-Score
  filter", with a dynamic helper below ("No filter" / "<grade> or better")
  that mirrors aria-valuetext for screen-reader parity.
- Storage collapsed: removed STORAGE_KEY_NUTRISCORE_ENABLED;
  STORAGE_KEY_NUTRISCORE_MAX is absent when the slider is at Off.

Tests updated: 14/14 pass.
@kiranannadatha8
Copy link
Copy Markdown
Contributor Author

I'm thinking if we could remove the toggle completely by adding an option to the slider (something like "off" or "no filter") that turned off the search

Thanks for the review, @rolandgeider — applied your suggestion in the follow-up commit.

nutriscore-filter-off:
nutriscore-filter-off

nutriscore-filter-C:
nutriscore-filter-C

The slider expresses the "better than X" semantic cleanly. "Only X" is only incidentally supported at A (since A is the top grade). If exact-match filtering becomes a product requirement later, happy to follow up with a secondary control (chips or a mode selector) — but the backend's nutriscore__exact lookup is already in place from #2305, so we'd only be adding UI on top.

@rolandgeider
Copy link
Copy Markdown
Member

awesome, this looks much cleaner

kiranannadatha8 added a commit to kiranannadatha8/flutter that referenced this pull request Apr 20, 2026
Mirrors the React refactor in wger-project/react#1237 addressing
Roland's review — the SwitchListTile + conditional Slider pair
expressed one semantic decision ("what grade ceiling?") through two
controls. Folded "Off" into the slider itself:

- Slider stops are now Off | A | B | C | D | E (was A..E, gated by
  a SwitchListTile).
- Off position sends no `nutriscoreMax`; A..E map as before.
- Heading changed from "Filter by Nutri-Score" to "Nutri-Score
  filter", with a dynamic helper ("No filter" / "<grade> or better").
- Model: `IngredientFilters.nutriscoreMax` is now nullable; removed
  `filterNutriscore`. `copyWith` takes a `clearNutriscoreMax` flag
  because Dart's `??` cannot distinguish "no argument" from "null".
- Storage collapsed: `saveIngredientNutriscoreMax(null)` removes the
  key; `getIngredientNutriscoreMax()` returns `null` when absent.
- Riverpod notifier: removed `toggleNutriscore`; `chooseNutriscoreMax`
  now takes `NutriScore?`.
- l10n: removed `filterNutriscoreMax`; added `filterNutriscoreOff`,
  `filterNutriscoreNoFilter`, `filterNutriscoreOrBetter` (placeholder).

Tests updated: 61/61 pass.
@rolandgeider rolandgeider merged commit e2ef640 into wger-project:master Apr 20, 2026
5 checks passed
@rolandgeider
Copy link
Copy Markdown
Member

merged, thank you!

(we probably don't need any refinements on these filters in the future, realistically I don't think anybody is going to want to search for "only A or C" or "worse than C")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants