Frontend: B1 — Recipe library #22

Closed
opened 2026-04-02 11:27:11 +02:00 by marcel · 6 comments
Owner

Summary

Browse all recipes in a card grid. Hub for recipe discovery and creation. Entry point for J1 (Add a recipe).

Journey: J1 — Add a recipe
Role: Planner only
Screen: B1

Layout

Mobile (< 768px)

  • Topbar: "Recipes" title + search icon + add (+) icon
  • 2-column card grid, 8px gap
  • Bottom tab bar

Desktop (> 1024px)

  • Sidebar (224px) + topbar (search input 220px + "Add recipe" button) + filter chips row + 4-column card grid (12px gap)

Recipe Cards

Mobile

  • 64px image area (placeholder if no image)
  • Recipe name (truncated)
  • Metadata row (time, effort)
  • Tags omitted on mobile (too small)

Desktop

  • 100px image area
  • Recipe name
  • Metadata row
  • Tags visible (effort, category chips)

Filter Chips

  • Displayed as a row above the grid (desktop) or collapsible (mobile)
  • Options: All (count), Easy, Medium, Hard, Chicken, Fish, Vegetarian, Pasta, etc.
  • Active chip: --green-tint bg + --green-dark text
  • Inactive chip: --color-border border
  • Chip style: 11px weight 500, 5px 14px padding, 12px radius

Behavior

  • Card click → navigates to B2 (recipe detail)
  • Add button → navigates to B3 (new recipe form, empty state)
  • Filter chips filter the grid by tag
  • Search filters by recipe name

Acceptance Criteria

  • Mobile: 2-column grid with 64px image cards
  • Desktop: 4-column grid with 100px image cards + filter chips
  • Filter chips filter by effort and category tags
  • Search by recipe name
  • Card click → B2, Add → B3
## Summary Browse all recipes in a card grid. Hub for recipe discovery and creation. Entry point for J1 (Add a recipe). **Journey:** J1 — Add a recipe **Role:** Planner only **Screen:** B1 ## Layout ### Mobile (< 768px) - Topbar: "Recipes" title + search icon + add (+) icon - 2-column card grid, 8px gap - Bottom tab bar ### Desktop (> 1024px) - Sidebar (224px) + topbar (search input 220px + "Add recipe" button) + filter chips row + 4-column card grid (12px gap) ## Recipe Cards ### Mobile - 64px image area (placeholder if no image) - Recipe name (truncated) - Metadata row (time, effort) - Tags **omitted** on mobile (too small) ### Desktop - 100px image area - Recipe name - Metadata row - Tags visible (effort, category chips) ## Filter Chips - Displayed as a row above the grid (desktop) or collapsible (mobile) - Options: All (count), Easy, Medium, Hard, Chicken, Fish, Vegetarian, Pasta, etc. - Active chip: `--green-tint` bg + `--green-dark` text - Inactive chip: `--color-border` border - Chip style: 11px weight 500, 5px 14px padding, 12px radius ## Behavior - Card click → navigates to B2 (recipe detail) - Add button → navigates to B3 (new recipe form, empty state) - Filter chips filter the grid by tag - Search filters by recipe name ## Acceptance Criteria - [ ] Mobile: 2-column grid with 64px image cards - [ ] Desktop: 4-column grid with 100px image cards + filter chips - [ ] Filter chips filter by effort and category tags - [ ] Search by recipe name - [ ] Card click → B2, Add → B3
marcel added the kind/featurepriority/medium labels 2026-04-02 11:30:04 +02:00
Author
Owner

Spec file: specs/frontend/j1-add-recipe.html — screen B1 with mobile (2-col grid) + desktop (4-col grid + filter chips) previews, agent table, and LLM implementation guide.

**Spec file:** [`specs/frontend/j1-add-recipe.html`](../specs/frontend/j1-add-recipe.html) — screen B1 with mobile (2-col grid) + desktop (4-col grid + filter chips) previews, agent table, and LLM implementation guide.
Author
Owner

👨‍💻 Kai — Frontend Engineer

B1 is a data-heavy grid with filtering and search — good scope for breaking it into focused components. Here's how I'd think about the implementation:

Component split I'd propose:

  • RecipeLibraryPage.svelte — top-level, owns $state for active filter + search query
  • RecipeTopbar.svelte — title + search icon (mobile) / search input (desktop) + add button
  • FilterChipRow.svelte — chip list, emits selected filter back up
  • RecipeGrid.svelte — receives recipes prop, renders cards in the correct column count
  • RecipeCard.svelte — single card, two display variants via a compact prop (mobile 64px vs desktop 100px)

Questions before I start:

  • Are filter chips and search combined with AND logic (recipe must match filter AND search term), or are they independent? The issue doesn't say.
  • Filter chips say "Chicken, Fish, Vegetarian, Pasta, etc." — are these driven by recipe tags returned from the backend, or is it a hardcoded list? If backend-driven, the chips need to be loaded from the API.
  • "Collapsible" filter chips on mobile — is that a drawer, a toggle row, or a horizontal scroll? The spec says collapsible but doesn't define the pattern.
  • What's the empty state when no recipes exist yet? And the empty state when filters return zero results? These need different UI treatment.
  • The "+page.server.ts" load function — should it fetch all recipes upfront and filter client-side, or should filter/search params be sent as query params to the backend? For large libraries, client-side filtering won't scale.

Risks I see:

  • The 2-column / 4-column switch via Tailwind grid-cols-2 lg:grid-cols-4 is straightforward, but I want to confirm the breakpoints match Atlas's spec (768px / 1024px). SvelteKit SSR won't know the viewport, so the grid must be CSS-only — no JS-based column detection.
  • If we filter client-side, data loading in +page.server.ts should fetch with pagination or a reasonable limit. An unbounded "give me all recipes" call will bite us later.
  • RecipeCard needs an explicit alt text strategy for images — what do we show as alt text when there's no recipe image and we're rendering a placeholder?
## 👨‍💻 Kai — Frontend Engineer B1 is a data-heavy grid with filtering and search — good scope for breaking it into focused components. Here's how I'd think about the implementation: **Component split I'd propose:** - `RecipeLibraryPage.svelte` — top-level, owns `$state` for active filter + search query - `RecipeTopbar.svelte` — title + search icon (mobile) / search input (desktop) + add button - `FilterChipRow.svelte` — chip list, emits selected filter back up - `RecipeGrid.svelte` — receives `recipes` prop, renders cards in the correct column count - `RecipeCard.svelte` — single card, two display variants via a `compact` prop (mobile 64px vs desktop 100px) **Questions before I start:** - Are filter chips and search combined with AND logic (recipe must match filter AND search term), or are they independent? The issue doesn't say. - Filter chips say "Chicken, Fish, Vegetarian, Pasta, etc." — are these driven by recipe tags returned from the backend, or is it a hardcoded list? If backend-driven, the chips need to be loaded from the API. - "Collapsible" filter chips on mobile — is that a drawer, a toggle row, or a horizontal scroll? The spec says collapsible but doesn't define the pattern. - What's the empty state when no recipes exist yet? And the empty state when filters return zero results? These need different UI treatment. - The "+page.server.ts" load function — should it fetch all recipes upfront and filter client-side, or should filter/search params be sent as query params to the backend? For large libraries, client-side filtering won't scale. **Risks I see:** - The 2-column / 4-column switch via Tailwind `grid-cols-2 lg:grid-cols-4` is straightforward, but I want to confirm the breakpoints match Atlas's spec (768px / 1024px). SvelteKit SSR won't know the viewport, so the grid must be CSS-only — no JS-based column detection. - If we filter client-side, data loading in `+page.server.ts` should fetch with pagination or a reasonable limit. An unbounded "give me all recipes" call will bite us later. - `RecipeCard` needs an explicit `alt` text strategy for images — what do we show as alt text when there's no recipe image and we're rendering a placeholder?
Author
Owner

🔧 Backend Engineer — Recipe Library (B1)

Solid screen definition. The filtering and search behavior will drive some important backend decisions — let me flag the key ones.

API shape questions:

  • Is this endpoint already specced in the OpenAPI doc? The screen mentions filter by effort + category tags and search by name. That likely means GET /recipes?tag=Vegetarian&effort=Easy&search=pasta. We should agree on the exact query parameter names before the frontend builds against it.
  • Are filter chips driven by a static enum (effort: Easy/Medium/Hard) or by dynamic tags stored in the DB? If tags are user-defined, we need a GET /recipes/tags endpoint to populate the chips. If they're static, document that contract clearly.
  • Pagination: the issue says nothing about it, but a recipe library will grow. Are we returning all recipes in one shot for v1, or implementing cursor/page-based pagination now? I'd at least add a limit and offset parameter to the endpoint even if the UI doesn't expose paging yet.

Search implementation:

  • search by recipe name — is this a SQL ILIKE '%term%' on name? That's fine for v1 but won't use an index well. A gin index on to_tsvector(name) would scale better. Worth deciding now rather than retrofitting later.

Data concerns:

  • The card shows image, name, time, effort, and tags. The list endpoint should return a lightweight DTO — not the full recipe with ingredients and steps. Make sure the RecipeListItemDTO is defined separately from the RecipeDetailDTO that B2 uses.
  • Who owns the image storage? If recipes can have user-uploaded images, we need to decide where images live (filesystem, object storage, DB blob?) and what the card receives — a URL or an ID? This is load-bearing for both the backend and the card component.

Questions:

  • Planner-only access is stated — is the recipe list endpoint restricted at the household level (you only see your household's recipes), or is there a global recipe library? The answer affects the authorization logic significantly.
## 🔧 Backend Engineer — Recipe Library (B1) Solid screen definition. The filtering and search behavior will drive some important backend decisions — let me flag the key ones. **API shape questions:** - Is this endpoint already specced in the OpenAPI doc? The screen mentions filter by effort + category tags and search by name. That likely means `GET /recipes?tag=Vegetarian&effort=Easy&search=pasta`. We should agree on the exact query parameter names before the frontend builds against it. - Are filter chips driven by a static enum (effort: Easy/Medium/Hard) or by dynamic tags stored in the DB? If tags are user-defined, we need a `GET /recipes/tags` endpoint to populate the chips. If they're static, document that contract clearly. - Pagination: the issue says nothing about it, but a recipe library will grow. Are we returning all recipes in one shot for v1, or implementing cursor/page-based pagination now? I'd at least add a `limit` and `offset` parameter to the endpoint even if the UI doesn't expose paging yet. **Search implementation:** - `search` by recipe name — is this a SQL `ILIKE '%term%'` on `name`? That's fine for v1 but won't use an index well. A `gin` index on `to_tsvector(name)` would scale better. Worth deciding now rather than retrofitting later. **Data concerns:** - The card shows image, name, time, effort, and tags. The list endpoint should return a lightweight DTO — not the full recipe with ingredients and steps. Make sure the `RecipeListItemDTO` is defined separately from the `RecipeDetailDTO` that B2 uses. - Who owns the image storage? If recipes can have user-uploaded images, we need to decide where images live (filesystem, object storage, DB blob?) and what the card receives — a URL or an ID? This is load-bearing for both the backend and the card component. **Questions:** - Planner-only access is stated — is the recipe list endpoint restricted at the household level (you only see your household's recipes), or is there a global recipe library? The answer affects the authorization logic significantly.
Author
Owner

🧪 QA Engineer — Recipe Library (B1)

B1 has a deceptively large test surface. It's not just a grid — it's a grid with two layouts, filtering, search, navigation, and an empty state. Let me map out the paths I'll want covered.

Component test coverage (Vitest + Testing Library):

  • RecipeCard: renders name, time, effort; truncates long names; shows placeholder when no image; clicking fires navigation
  • FilterChipRow: renders all chip options; clicking a chip marks it active (correct styling class or aria-pressed); clicking active chip deactivates (if toggle behavior is intended)
  • RecipeGrid: renders correct number of cards from props; renders empty state when recipes is an empty array
  • Search input: typing updates filtered results; clearing input restores full list

Integration / E2E paths I want covered:

Path Test type
Load page — recipes displayed in grid E2E (happy path)
Click filter chip → grid updates Component or E2E
Search by name → grid filters Component or E2E
Click card → navigates to B2 E2E
Click Add → navigates to B3 E2E
Empty library (no recipes yet) Component
Filter returns zero results Component
Mobile layout: 2 columns visible E2E (mobile viewport)
Desktop layout: 4 columns + filter row E2E (desktop viewport)

Questions / gaps I see:

  • What happens when the API call fails? Is there an error state, or does the grid just show nothing? We need a test for that and a visible error message.
  • Are search and filter combined? If so, I need to test that combination — filter=Vegetarian AND search="pasta" returns only vegetarian pasta dishes.
  • "Tags omitted on mobile" — is this purely CSS (visually hidden) or are tags excluded from the mobile card DOM entirely? If they're hidden with CSS, a screen reader still reads them. If excluded from DOM, we need to verify the correct markup at each breakpoint.
  • The "collapsible" filter chip behavior on mobile: what triggers it, what's the collapsed/expanded state, and does it trap focus properly? This needs its own interaction test.
## 🧪 QA Engineer — Recipe Library (B1) B1 has a deceptively large test surface. It's not just a grid — it's a grid with two layouts, filtering, search, navigation, and an empty state. Let me map out the paths I'll want covered. **Component test coverage (Vitest + Testing Library):** - `RecipeCard`: renders name, time, effort; truncates long names; shows placeholder when no image; clicking fires navigation - `FilterChipRow`: renders all chip options; clicking a chip marks it active (correct styling class or aria-pressed); clicking active chip deactivates (if toggle behavior is intended) - `RecipeGrid`: renders correct number of cards from props; renders empty state when `recipes` is an empty array - Search input: typing updates filtered results; clearing input restores full list **Integration / E2E paths I want covered:** | Path | Test type | |---|---| | Load page — recipes displayed in grid | E2E (happy path) | | Click filter chip → grid updates | Component or E2E | | Search by name → grid filters | Component or E2E | | Click card → navigates to B2 | E2E | | Click Add → navigates to B3 | E2E | | Empty library (no recipes yet) | Component | | Filter returns zero results | Component | | Mobile layout: 2 columns visible | E2E (mobile viewport) | | Desktop layout: 4 columns + filter row | E2E (desktop viewport) | **Questions / gaps I see:** - What happens when the API call fails? Is there an error state, or does the grid just show nothing? We need a test for that and a visible error message. - Are search and filter combined? If so, I need to test that combination — filter=Vegetarian AND search="pasta" returns only vegetarian pasta dishes. - "Tags omitted on mobile" — is this purely CSS (visually hidden) or are tags excluded from the mobile card DOM entirely? If they're hidden with CSS, a screen reader still reads them. If excluded from DOM, we need to verify the correct markup at each breakpoint. - The "collapsible" filter chip behavior on mobile: what triggers it, what's the collapsed/expanded state, and does it trap focus properly? This needs its own interaction test.
Author
Owner

🔐 Sable — Security Engineer

B1 is a read-heavy screen, but there are a few threat vectors worth calling out before implementation starts.

Access control:

  • The spec says "Planner only" — I want to verify this restriction is enforced at the API layer in SecurityFilterChain or the service layer, not just by hiding the nav link from members. A member who knows the URL should get a 403, not a 200 with data.
  • The recipe list endpoint will return household-scoped recipes. We need to confirm the repository query always filters by household_id from the authenticated session — never from a request parameter that could be spoofed (IDOR risk).

Search input:

  • Search by recipe name goes to the backend as a query parameter. Even with parameterized SQL (which we must use — no string concatenation), I want to verify the search term length is bounded server-side (@Size(max = 100) or similar). Unbounded search strings can be a DoS vector against the DB.
  • Client-side: the search term will be rendered back into the UI (e.g., "No results for 'X'"). Make sure X is text-interpolated in Svelte, not injected via {@html} — otherwise we have a reflected XSS vector.

Image handling:

  • If recipe images are user-uploaded and served from a URL stored in the DB, we need to ensure the image src values are validated before storage (no javascript: URIs, no cross-household image URL leakage). This is a decision point that should be captured in the image upload spec.

Information leakage:

  • The card grid response DTO must not include fields like household_id, internal IDs beyond what the frontend needs for navigation, or any admin/audit fields. Confirm the RecipeListItemDTO is purpose-built and reviewed before the endpoint ships.

Questions:

  • Is search executed client-side or server-side? Client-side filtering of a full dataset has different risk characteristics than sending the search term to the backend. Either way needs explicit review.
## 🔐 Sable — Security Engineer B1 is a read-heavy screen, but there are a few threat vectors worth calling out before implementation starts. **Access control:** - The spec says "Planner only" — I want to verify this restriction is enforced at the API layer in `SecurityFilterChain` or the service layer, not just by hiding the nav link from members. A member who knows the URL should get a 403, not a 200 with data. - The recipe list endpoint will return household-scoped recipes. We need to confirm the repository query always filters by `household_id` from the authenticated session — never from a request parameter that could be spoofed (IDOR risk). **Search input:** - Search by recipe name goes to the backend as a query parameter. Even with parameterized SQL (which we must use — no string concatenation), I want to verify the search term length is bounded server-side (`@Size(max = 100)` or similar). Unbounded search strings can be a DoS vector against the DB. - Client-side: the search term will be rendered back into the UI (e.g., "No results for 'X'"). Make sure `X` is text-interpolated in Svelte, not injected via `{@html}` — otherwise we have a reflected XSS vector. **Image handling:** - If recipe images are user-uploaded and served from a URL stored in the DB, we need to ensure the image `src` values are validated before storage (no `javascript:` URIs, no cross-household image URL leakage). This is a decision point that should be captured in the image upload spec. **Information leakage:** - The card grid response DTO must not include fields like `household_id`, internal IDs beyond what the frontend needs for navigation, or any admin/audit fields. Confirm the `RecipeListItemDTO` is purpose-built and reviewed before the endpoint ships. **Questions:** - Is search executed client-side or server-side? Client-side filtering of a full dataset has different risk characteristics than sending the search term to the backend. Either way needs explicit review.
Author
Owner

🎨 Atlas — UI/UX Designer

B1 is the entry point for one of the most used journeys (J1 — Add a recipe) and the primary discovery surface. A few design clarifications needed before implementation starts.

Filter chips — spec alignment:

  • Chips use 11px weight 500, 5px 14px padding, 12px radius — this diverges from the default --radius-md (6px). I want to explicitly confirm 12px is intentional here (it would map to --radius-lg), not a copy error from another spec. Kai will need the exact token.
  • Active chip state correctly uses --green-tint bg + --green-dark text, matching the nav active pattern — good.
  • Inactive chip uses --color-border border — confirm this means a transparent/white background with a border stroke, not a filled background.

Mobile collapsible chips:

  • The spec says "collapsible" on mobile but gives no pattern. My recommendation: a single "Filter" button with the active count badge (e.g., "Filter · 2") that opens a bottom sheet or expands an inline row. Whatever pattern is chosen, it needs a defined open/closed visual state and a dismiss mechanism. I need to spec this before Kai builds it.

Card image areas:

  • 64px (mobile) and 100px (desktop) — are these square or 16:9? The spec says "image area" but doesn't give aspect ratio. I'd recommend square (1:1) for grid cards — more predictable layout. Placeholder should use --color-border background with a centered icon, not a broken image or blank space.

Empty states:

  • No empty state is defined in this issue. Two distinct empty states are needed: (1) no recipes in the library yet — should prompt the user to add their first recipe with the Add button prominent, (2) no results for current filter/search — should suggest clearing filters. Both need a visual design before implementation.

Typography check:

  • Recipe name truncation on mobile cards — confirm this is text-overflow: ellipsis on a single line, not a two-line clamp. Single line is safer at 64px card height.
## 🎨 Atlas — UI/UX Designer B1 is the entry point for one of the most used journeys (J1 — Add a recipe) and the primary discovery surface. A few design clarifications needed before implementation starts. **Filter chips — spec alignment:** - Chips use `11px weight 500, 5px 14px padding, 12px radius` — this diverges from the default `--radius-md (6px)`. I want to explicitly confirm `12px` is intentional here (it would map to `--radius-lg`), not a copy error from another spec. Kai will need the exact token. - Active chip state correctly uses `--green-tint` bg + `--green-dark` text, matching the nav active pattern — good. - Inactive chip uses `--color-border` border — confirm this means a transparent/white background with a border stroke, not a filled background. **Mobile collapsible chips:** - The spec says "collapsible" on mobile but gives no pattern. My recommendation: a single "Filter" button with the active count badge (e.g., "Filter · 2") that opens a bottom sheet or expands an inline row. Whatever pattern is chosen, it needs a defined open/closed visual state and a dismiss mechanism. I need to spec this before Kai builds it. **Card image areas:** - 64px (mobile) and 100px (desktop) — are these square or 16:9? The spec says "image area" but doesn't give aspect ratio. I'd recommend square (1:1) for grid cards — more predictable layout. Placeholder should use `--color-border` background with a centered icon, not a broken image or blank space. **Empty states:** - No empty state is defined in this issue. Two distinct empty states are needed: (1) no recipes in the library yet — should prompt the user to add their first recipe with the Add button prominent, (2) no results for current filter/search — should suggest clearing filters. Both need a visual design before implementation. **Typography check:** - Recipe name truncation on mobile cards — confirm this is `text-overflow: ellipsis` on a single line, not a two-line clamp. Single line is safer at 64px card height.
Sign in to join this conversation.