Skip to content

feat: recipe categories with filter sheet on list#166

Open
plusmobileapps wants to merge 6 commits into
mainfrom
feat-recipe-categories-and-filter
Open

feat: recipe categories with filter sheet on list#166
plusmobileapps wants to merge 6 commits into
mainfrom
feat-recipe-categories-and-filter

Conversation

@plusmobileapps
Copy link
Copy Markdown
Collaborator

Summary

Adds recipe menu categories to chef-mate. Two user-facing pieces:

  1. EditRecipeScreen now has a Category picker between Description and Star Rating: a flow row of FilterChips (one per category, plus a leading "None" chip) styled to match the existing sort/filter chip pattern. Tapping the selected chip clears it.
  2. RecipeListScreen's existing filter bottom sheet gains a new Category section beneath Sort and Filter — multi-select chips, sharing the sheet's existing Clear All + Apply controls. The filter icon's badge counts both legacy filters and category filters via Model.totalActiveFilterCount, so the user gets a single visible indicator that filters are active.

RecipeCategory is an enum with stable string IDs (NOT ordinals — same reasoning as the bottom-nav reorder PR; we don't want renumbering to corrupt persisted data). Defaults: breakfast, lunch, dinner, appetizer, side, dessert, snack, drink, other. Recipes can have at most one primary category. A null category counts as "uncategorized" for filtering and is matched only when the user includes OTHER in the filter set.

Selection persists across config changes / process death via the same Settings layer as the existing sort + filter prefs (key: recipe_list_active_categories, comma-separated stable IDs).

Schema changes

Local (SQLDelight)

Remote (Supabase) — manual task

A Supabase migration is included as a file in this PR but must be run manually before merging:

File: supabase/migrations/20260508_add_recipe_category.sql

ALTER TABLE recipes
    ADD COLUMN IF NOT EXISTS category TEXT;

Stored as free-form TEXT (no CHECK constraint) to keep parity with how the existing client_id / similar identifier columns are stored on this schema, and so future categories can be added on the client without a coordinated server migration. The client validates known category IDs via the RecipeCategory enum.

Apply command (Supabase CLI):

supabase db push --include supabase/migrations/20260508_add_recipe_category.sql

Or via psql:

psql "$SUPABASE_DB_URL" -f supabase/migrations/20260508_add_recipe_category.sql

Tasks:

  • Run migration 20260508_add_recipe_category.sql on staging
  • Run migration 20260508_add_recipe_category.sql on production

Sync

RemoteRecipe carries a category: String? field (column name category). The repository's three sync paths (push add, push dirty update, pull from remote) all round-trip the category, so editing a category on one device propagates to others.

Backfill / migration behaviour

Recipes created before this column was added will have null / "Other" category until the user edits them. They appear in:

  • The unfiltered list, exactly as before.
  • Any filter set that includes the Other category chip.

No data migration is performed — null is treated semantically as "uncategorized" rather than auto-assigned to OTHER, so we can distinguish "user explicitly chose Other" from "user hasn't set a category yet" if we ever want to.

Tests

Unit tests (./gradlew :client:recipe:list:impl:jvmTest :client:recipe:core:impl:jvmTest — passing locally):

  • EditRecipeViewModelTest: starting category is null, an existing recipe's category seeds the picker, selecting + saving persists the chosen category, clearing back to null persists null.
  • RecipeListViewModelTest: empty filter set returns everything, single-category filter, multi-category union, empty result, OTHER matches null-category recipes, OTHER-not-selected excludes them, category + legacy FAVORITES filter AND together, persistence round-trips through Settings, clearFilters() clears categories.

Screenshot tests (client/ui/screenshot-test/.../RecipeCategoryFilterScreenshotTest.kt):

  • Recipe list with an active category filter — light + dark — verifies the badge count and tinted filter icon.
  • Sort/filter sheet body with one category pre-selected — light + dark — locks in the new Category section layout.

SortFilterSheetContent was extracted from the ModalBottomSheet wrapper so the screenshot can render the panel directly (Dialog-backed composables don't render reliably under the screenshot plugin).

Conflicts to watch

Test plan

  • Create a recipe, pick a category, save — verify it appears with that category after reopening
  • Edit an existing pre-category recipe — verify category starts as "None"
  • On the list, open the filter sheet, pick two categories, Apply — verify only those recipes show
  • Verify the filter icon shows the count badge (2) and is tinted
  • Verify "Clear all" empties both filter and category chips
  • Pick Other, Apply — verify pre-category recipes match
  • Kill the app, relaunch — verify filters persist (matches existing search/sort behaviour)
  • Sign in / sync — verify category round-trips to/from Supabase (after running the manual migration)

🤖 Generated with Claude Code

@andrew-steinmetz
Copy link
Copy Markdown

@claude should i be creating a separate table in supabase that is just for the categories and make use of a foreign key in the recipe? using the category name as a string seems fragile if the user were to ever change the category name?

@plusmobileapps plusmobileapps force-pushed the feat-recipe-categories-and-filter branch 2 times, most recently from 5528d88 to a8b2ae7 Compare May 17, 2026 18:55
plusmobileapps and others added 5 commits May 17, 2026 12:48
Introduces a Category model and a many-to-many recipe ↔ category
relationship that supports built-in presets (Breakfast, Lunch, etc.)
alongside user-created categories.

- Schema: adds Category.sq and RecipeCategory.sq join table. Since the
  app hasn't shipped, the SQLDelight migration history is collapsed to
  v1 (deletes 1.sqm-6.sqm and old schema snapshots; only the .sq files
  remain as the source of truth).
- Data layer: Category data class, BuiltinCategory presets enum,
  CategoryRepository (offline-first local insert with background
  Supabase sync via CategoryRemoteDataSource), and a Set<Category> field
  on Recipe.
- RecipeRepository: filter API to query recipes by attached categories.
- Supabase: single migration creating categories + recipe_categories
  tables with row-level security policies scoped to the owner.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a category picker to the edit-recipe screen: tap a chip to open a
modal bottom sheet with checkboxes for each built-in preset plus the
user's own categories. A "+" button in the sheet header lets the user
create a new category inline; the local DB insert is offline-first and
the row appears in the list immediately, with remote sync happening in
the background. Materialized built-in rows are filtered out of the
user-categories section to avoid double-rendering.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a sort & filter modal bottom sheet to the recipe list, opened from
the filter icon in the app bar. The sheet has three sections:
- Sort (recently added, A→Z, top rated, etc.)
- Filter (favorites, rated, quick recipes)
- Filter by category — chips for each built-in preset plus the user's
  own categories. Selecting any chip filters the underlying list.

The sheet opens fully expanded (skipPartiallyExpanded = true) so the
filter sections aren't hidden behind a half-snap. An active filter
count is shown as a badge on the icon.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Renders attached categories as a FlowRow of small secondary-container
pills between the hero details and the description, across compact,
landscape, and expanded layouts. Built-in presets resolve via
pickerLabelRes() so labels follow the user's locale; user-created
categories use their stored name.

Adds focused snapshot coverage for the new RecipeCategoriesRow
composable (mixed light/dark + FlowRow wrapping at 360dp) and wires
the screenshot-test module's missing recipe:core/data deps.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Locks in the steady states for the new category surfaces:
- CategoryPickerContent — empty + populated-with-selection, light/dark
- SortFilterSheetContent — populated with user categories + with a
  category selected, light/dark
- Recipe list rendered with an active category filter, light/dark

ModalBottomSheet doesn't render under the screenshot test plugin, so
the picker/filter tests target the sheet content directly. The inline
create-row state machine is left to viewmodel unit tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@plusmobileapps plusmobileapps force-pushed the feat-recipe-categories-and-filter branch from c22b42e to 7ea4a0b Compare May 17, 2026 19:50
References were left over from a recording done before the picker row
gained heightIn(min = 48.dp) + Modifier.toggleable, so CI's render no
longer matched the committed PNGs. Replaced the four CategoryPicker
references with CI's rendered output to bring them back in sync.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

2 participants