A small product discovery page for the Downshift founding engineer build task. The app helps someone search and browse about 4,000 home goods products without trying to become a full ecommerce store.
The prompt emphasized that the decisions around search matter more than search complexity, so this project focuses on a polished, fast, explainable discovery flow: browse first, search when needed, refine with lightweight filters, and keep imperfect catalog data from breaking the experience.
- A standalone Vite, React, TypeScript, Tailwind CSS, shadcn-style UI, TanStack Query, and nuqs app.
- A catalog page with a bold editorial home-goods visual direction.
- Client-side catalog fetching and caching through TanStack Query.
- Client-side search across title, brand, category, tags, and description.
- Filter controls for search, category, availability, price ranges, custom price, and popular tags.
- Sort controls for best selling, relevance, price low to high, price high to low, and top rated.
- Client-side pagination with adjacent-page and hover/page-input prefetching.
- URL-driven filter state with nuqs so filtered views are refresh-safe and shareable.
- Desktop filter rail and a mobile filter drawer so mobile users see products before filters.
- Product cards with image fallback, price fallback, rating/review display, brand/category context, and accessible interactions.
The task asked for a small product discovery page, not a full store. I intentionally kept the surface area focused:
- No cart, checkout, authentication, or product detail pages.
- No backend search service because 4,000 products is small enough for fast local filtering.
- No complex ranking model because the search behavior should be easy to reason about in a short walkthrough.
- Enough polish to make the results feel useful and production-minded.
This let me spend the time on the important product decisions: how users start browsing, how search ranks matches, how filters combine, how mobile behaves, and how messy data is handled.
Search is local, fast, and deliberately explainable.
- Title matches rank highest because they usually represent direct user intent.
- Brand and tag matches rank next because shoppers often search by maker, material, style, or product attribute.
- Category matches are useful, but less specific than direct title/tag matches.
- Description matches are included as a softer recall signal.
- In-stock status, rating, and review volume help default browsing feel curated without making release date recency the main signal.
When there is no query, the page behaves like a catalog browse experience instead of an empty search tool. Users can immediately scan products, sort the catalog, paginate, or open filters to narrow by category, availability, price, and tags.
The incoming catalog data is normalized before rendering so the UI can tolerate imperfect product records.
- Number, string, and comma-formatted prices are coerced into nullable numbers.
- Missing prices render safely instead of breaking sorting or cards.
- Missing images use a designed placeholder.
- Missing descriptions fall back to neutral display copy.
- Missing ratings are handled without pretending every product has a score.
- Future
releasedAtvalues are preserved but are not treated as the primary ranking signal.
The provided catalog is mirrored into public/items.json for reliable local/static app loading, then fetched client-side through TanStack Query.
- TanStack Query caches the catalog payload.
- Derived result pages are cached by search/filter/sort/page query keys.
- Adjacent result pages are prefetched after page load.
- Pagination buttons prefetch on hover/focus.
- Page-jump input prefetches valid typed pages before the user clicks Go.
- Filter state lives in the URL through nuqs rather than localStorage, avoiding competing persistence sources.
On desktop, the filter rail stays visible because there is enough room for browsing and refinement side-by-side.
On mobile, the product grid comes first. Filters move into a floating button and native dialog drawer, so the first screen is not dominated by the full filter panel. This keeps mobile discovery closer to how a real shopper would browse.
pnpm install
pnpm devThe dev server can be run on a specific port with:
pnpm dev -- --host 127.0.0.1 --port 3003pnpm test
pnpm lint
pnpm buildUse this order for the 10-minute max recording requested in the prompt:
- Open by restating the prompt: build a small product discovery page for 4,000 home goods items, not a full store.
- Explain the main product decision: optimize for useful discovery and explainable search decisions.
- Show the default catalog state and why users can browse immediately.
- Demo a search, such as
linen,folding,rattan, orlamp. - Explain weighted matching across title, brand, category, tags, and description.
- Demo filters: category, availability, price, custom price validation, and tags.
- Demo active filter chips and clear/reset behavior.
- Demo sorting and pagination.
- Point out TanStack Query caching, page prefetching, and URL state via nuqs.
- Show mobile mode and the filter drawer.
- Explain data normalization and fallback handling.
- Close with the next step and tradeoff below.
The next step would be adding lightweight search analytics: track zero-result queries, common refinements, and which filters users remove. That would show whether the ranking and filter model matches real shopper behavior before making the search algorithm more complex.
The main tradeoff is client-side search and filtering. For about 4,000 items, it keeps the app fast, simple, and easy to reason about. If the catalog grew much larger, I would move filtering/ranking/pagination server-side or use a dedicated search service with typo tolerance and synonym support.