Repository navigation
feat(catalog): categories, search and filters (spec 0013) - #21
Merged
Merged
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 taggedcatalogandcategories. It storesinStockas a boolean, never the stock count.buildListing()does search, variant level filters, facet counts, sorting and paging as pure functions.parseListingParams()andlistingHref()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.tsaddsfoldText,searchWordsandfacetKey, sokosciolfinds "Kościół" and "Colour" / "COLOUR" become one filter group.Storefront
/categories/[slug]and/search. Home/becomes the full listing; the 48 product cap andgetActiveProductsare gone.router.pushso Back undoes the last change. Without it, the panel is a form with an Apply button./searchisnoindex, followand 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 newreorderCategoryProductsaction locks the category row, refuses withstaleunless the submitted ids match the current set exactly, and rewrites positions0..n-1in one statement.updateProductCategoriesandcreateProduct) now expires the newcategoriescache tag, so changes show on the next storefront request.How to test / verify
pnpm test: pure rules insrc/lib/search-text.test.tsandsrc/features/catalog/listing/*.test.ts(search folding, matching and ranking, filters, counts, params repair, paging).pnpm test:db(needspnpm db:start):tests/db/catalog-listing.db.test.tsandtests/db/categories.db.test.ts(index contents, cache tags, the reorder action and itsstalepath).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/searchwith filters open.kosciolfor a product named "Kościół"; open/categories/<slug>?min=50&max=10&page=999&sort=zzzand 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
useSearchParamsand Suspense in every header);/searchhas its own prefilled box instead.tabTointests/e2e/a11y.tsnow 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