Skip to content

feat(catalog): categories, search and filters (spec 0013) - #21

Merged
kris1027 merged 21 commits into
mainfrom
feat/categories-search-filters
Oct 17, 2026
Merged

kris1027 merged 21 commits into
mainfrom
feat/categories-search-filters

Conversation

@kris1027

Copy link
Copy Markdown
Owner

What

Shoppers can now find products three ways: a page per visible category (linked from the header), a name search (a box in the header), and a home page that lists the whole catalog with paging. Every listing shares the same filters (price, option values like size and colour, in stock, on sale) and sort orders, and the URL holds the full state. Admins can also arrange the products inside a category.

Why

Implements docs/specs/0013-categories-search-filters/index.md (feature 13 on the scope). At a few hundred products, the spec picks one cached listing index filtered in plain code per request: no schema change and no database query per search. Options and reasoning are in rationale.md.

Changes

Listing engine (pure, in src/features/catalog/listing/)

  • getListingIndex() reads every listable product (active, with at least one live variant) once into a 'use cache' entry tagged catalog and categories. It stores inStock as a boolean, never the stock count.
  • buildListing() does search, variant level filters, facet counts, sorting and paging as pure functions.
  • parseListingParams() and listingHref() turn the URL into state and back. Bad params are repaired or dropped silently, and one state always has one URL (stable param order, defaults left out).
  • src/lib/search-text.ts adds foldText, searchWords and facetKey, so kosciol finds "Kościół" and "Colour" / "COLOUR" become one filter group.

Storefront

  • New routes /categories/[slug] and /search. Home / becomes the full listing; the 48 product cap and getActiveProducts are gone.
  • Header gets the category nav (desktop and mobile menu) and a search box that works as a plain GET form without JavaScript.
  • New listing UI: filter panel (sidebar on desktop, Sheet on mobile), removable filter chips, sort select, numbered pagination, empty states, loading skeleton.
  • With JavaScript, filters apply instantly via router.push so Back undoes the last change. Without it, the panel is a form with an Apply button.
  • SEO: category titles and descriptions use the meta fields with fallbacks, /search is noindex, follow and never puts query text in metadata, and home and category pages declare their plain path as canonical.

Admin

  • /admin/categories/<id> turns its product list into the shared sortable list with an explicit Save order. The new reorderCategoryProducts action locks the category row, refuses with stale unless the submitted ids match the current set exactly, and rewrites positions 0..n-1 in one statement.
  • Every category write (create, update, delete, reorder, set products, the arrange action, plus updateProductCategories and createProduct) now expires the new categories cache tag, so changes show on the next storefront request.

How to test / verify

  • pnpm test: pure rules in src/lib/search-text.test.ts and src/features/catalog/listing/*.test.ts (search folding, matching and ranking, filters, counts, params repair, paging).
  • pnpm test:db (needs pnpm db:start): tests/db/catalog-listing.db.test.ts and tests/db/categories.db.test.ts (index contents, cache tags, the reorder action and its stale path).
  • pnpm test:e2e tests/e2e/catalog/discovery.spec.ts: the discovery flows, filters, search and category arrange on desktop and mobile, plus axe on home, a category and /search with filters open.
  • By hand, you might try: search kosciol for a product named "Kościół"; open /categories/<slug>?min=50&max=10&page=999&sort=zzz and see the range swapped, the last page and Featured order; copy a filtered URL into a private window and get the same results.

All 25 acceptance criteria were ticked in verify.md. Some value sourcing checks there are still open (unticked).

Risk & rollout

No migrations, no flags. The only write is product_categories.position, through the new admin action. The home page changes from a capped grid to the full paged listing, and every storefront page now reads the cached category nav. Rollback is a plain revert.

Notes for reviewers

  • The whole catalog is filtered in memory per request. That is the deliberate call for a few hundred products (see the rationale); it would need revisiting for a much larger catalog.
  • Under Cache Components, an unknown or hidden category renders the not found page with status 200, the same as the product page today (AC-2).
  • The header search box never prefills (that would need useSearchParams and Suspense in every header); /search has its own prefilled box instead.
  • Facet counts for one group ignore that group's own selection, so counts inside a group can add up to more than the total. That is intended (AC-14).
  • tabTo in tests/e2e/a11y.ts now allows up to 200 presses, because category links in the header add tab stops and local e2e runs leave many categories behind.

🤖 Generated with Claude Code

kris1027 and others added 7 commits October 16, 2026 19:52
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cked

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-project-automation github-project-automation Bot moved this to Backlog in BeStore Oct 16, 2026
@kris1027 kris1027 self-assigned this Oct 16, 2026
@kris1027 kris1027 added feature New user-facing functionality test Automated tests and test infrastructure ui Layouts, styling, and shared interface components labels Oct 16, 2026
@kris1027 kris1027 moved this from Backlog to In review in BeStore Oct 16, 2026
kris1027 and others added 14 commits October 17, 2026 13:32
Tracking params (utm_*, fbclid, gclid) counted toward the 10 group cap
in the parser, so a shared link could silently lose a real filter. The
parser now only caps values per group; buildListing caps groups after
matching them against the listing's own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parseMoney only takes a point, so a shopper typing 10,50 saw the value
silently dropped. The listing parser now reads one decimal comma as a
point before parseMoney; the URL keeps the point form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sort, stock, sale, q and the price bounds now go through Zod schemas
with .catch, like page, so every external input is validated as the
rules ask while bad input still repairs instead of failing. The search
page reads q through the new searchQuery instead of its own first-value
handling, keeping the route thin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
buildListing takes its scope and default order from a scopeOf switch
instead of two lets, a facet group's product count is the sum of its
label counts instead of a mutable field, and buildIndexProduct finds or
appends a merged type or value through one slotFor helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- listingLinks binds path, kind and currency once; the view hands
  NoResults a clear href and Pagination a page href instead of the
  kind, path and state clump.
- ListingKind derives from ListingScope, now beside it in params.ts.
- One PriceBounds, one ListingHeading, VariantFilters a Pick of
  ListingState, and one productCount label instead of three plurals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… counts

toCard turned inStock back into a fake stock of 1 or 0 to reuse the
admin summary. summarizeCard reads inStock directly, and the On sale
filter and the card's Sale badge now share one isSaleVariant rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
category-products.tsx held add and remove, the draft order and its
resync, the save bar and the discard dialog. The draft order is now
useDraftOrder, the sortable list and save bar ArrangedProducts, and the
dialog DiscardOrderDialog; CategoryProducts only wires the actions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…shows

The inline box appeared from lg while the desktop nav appears from md,
so between 768 and 1024px search hid behind the mobile button. It now
shows from md (narrower until lg), as AC-6 asks for desktop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The results bar repeated the panel's Apply. The sort select already
joins the filter form, so the panel's button submits both, as AC-19's
single Apply asks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ilter count

Locks the verify steps that only had manual checks: rename and
description on the next request, meta_title over the name, the plain
canonical on a paged and filtered URL, the home and search titles, and
four active filters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…steps

- AC-3: the desktop nav scrolls sideways when names overflow.
- AC-15: one decimal comma reads as a point.
- AC-18: the group cap counts only known groups.
- AC-20, AC-21: Clear all and Clear filters keep the sort too; the
  Sheet has a Show N products close button.
- verify.md: each value sourcing step names the test that proves it.
- AGENTS.md: the new listing and arranging files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A production build serves its CSS from _next/static/chunks too, so
aborting the whole folder left CI with an unstyled page where the fill
images covered the filter panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kris1027
kris1027 merged commit be1ccc0 into main Oct 17, 2026
1 check passed
@github-project-automation github-project-automation Bot moved this from In review to Done in BeStore Oct 17, 2026
@kris1027
kris1027 deleted the feat/categories-search-filters branch October 17, 2026 12:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New user-facing functionality test Automated tests and test infrastructure ui Layouts, styling, and shared interface components

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant