What
Make the products search bar fetch while you type, instead of waiting for Enter or the search button.
Every client-filtered surface in the app is already live: updates, map, suggestions, digital-cards, shopping-lists, watchlist all pass autoSearch to the shared search bar. The four surfaces backed by the network product search are the only ones still submit-on-Enter:
frontend/src/components/custom/header/components/header-search.tsx
frontend/src/components/custom/sidebar/app-sidebar.tsx
frontend/src/app/(root)/components/sections/hero-actions.tsx (landing hero)
frontend/src/app/products/components/products-client.tsx and frontend/src/components/custom/products-sheet/products-sheet.tsx
Why it is not just flipping autoSearch
The mechanism exists at frontend/src/components/custom/search/search-bar.tsx:84, but it fires on every keystroke with no debounce and calls router.replace. That is harmless for the six pages that filter a local array. For products it would mean, per character typed:
- A URL write, so the products page re-reads
q and refetches.
- One upstream call to
api.cijene.dev, and often two: frontend/src/lib/cijene-api/utils/product-search-fallback.ts runs an exact pass, then a second fuzzy: true pass when the exact pass returns fewer than 10 hits and q.length >= 3. Partial words are exactly the case that trips the fallback, so mid-typing queries are the expensive ones.
- A fan-out of per-EAN price requests from
use-filtered-product-prices.ts.
And useGetProductByName (frontend/src/lib/cijene-api/query-hooks.ts:79) has no placeholderData, so every new query key drops back to isLoading and blanks the list. Typing would flash the results empty on each character.
Work needed
Difficulty
Medium. The frontend plumbing is a handful of small changes and is genuinely easy. The risk is not the code, it is the request volume against an external API we do not own and cannot rate limit from the server side, plus the fuzzy fallback doubling calls precisely on partial queries.
Worth considering as a cheaper middle ground: keep the fetch on submit, but add a local suggestions dropdown fed by already-cached results, so typing feels live without new network calls. Related open work is #45 (batch price history) which would reduce the same fan-out.
Notes
docs/SEARCH.md §9 records that the Spring backend has no search at all, so there is no autocomplete or typeahead endpoint to lean on, ours or upstream. Upstream /v1/products takes q, fuzzy and limit only, capped at 100 rows, with no paging.
- If this ships,
docs/SEARCH.md needs updating.
What
Make the products search bar fetch while you type, instead of waiting for Enter or the search button.
Every client-filtered surface in the app is already live:
updates,map,suggestions,digital-cards,shopping-lists,watchlistall passautoSearchto the shared search bar. The four surfaces backed by the network product search are the only ones still submit-on-Enter:frontend/src/components/custom/header/components/header-search.tsxfrontend/src/components/custom/sidebar/app-sidebar.tsxfrontend/src/app/(root)/components/sections/hero-actions.tsx(landing hero)frontend/src/app/products/components/products-client.tsxandfrontend/src/components/custom/products-sheet/products-sheet.tsxWhy it is not just flipping
autoSearchThe mechanism exists at
frontend/src/components/custom/search/search-bar.tsx:84, but it fires on every keystroke with no debounce and callsrouter.replace. That is harmless for the six pages that filter a local array. For products it would mean, per character typed:qand refetches.api.cijene.dev, and often two:frontend/src/lib/cijene-api/utils/product-search-fallback.tsruns an exact pass, then a secondfuzzy: truepass when the exact pass returns fewer than 10 hits andq.length >= 3. Partial words are exactly the case that trips the fallback, so mid-typing queries are the expensive ones.use-filtered-product-prices.ts.And
useGetProductByName(frontend/src/lib/cijene-api/query-hooks.ts:79) has noplaceholderData, so every new query key drops back toisLoadingand blanks the list. Typing would flash the results empty on each character.Work needed
setTimeoutis the form-draft flush inhooks/use-form-draft.ts:108. Roughly 300 to 400 ms feels right, to be tuned.placeholderData: keepPreviousDataonuseGetProductByName, so the previous results stay on screen while the next query resolves. Pair it with anisFetchingaffordance rather than a full skeleton.MIN_FUZZY_QUERY_LENGTH. Also trim before theenabledcheck:enabled: Boolean(params.q)currently passes whitespace.router.replacevspushwhile typing. It has to bereplace, otherwise every keystroke becomes a history entry and Back is unusable.useSearchNavigationalready does this forsyncQuery.search-action-button.tsx. ItsisUnchangeddisabled state is meaningless once the URL tracks typing, so the button either becomes a no-op or changes role./productsfrom another route, so live typing there means navigating away mid-word. Probably keep them submit-on-Enter and only make the products page and the products sheet live.CIJENE_API_TOKEN).Difficulty
Medium. The frontend plumbing is a handful of small changes and is genuinely easy. The risk is not the code, it is the request volume against an external API we do not own and cannot rate limit from the server side, plus the fuzzy fallback doubling calls precisely on partial queries.
Worth considering as a cheaper middle ground: keep the fetch on submit, but add a local suggestions dropdown fed by already-cached results, so typing feels live without new network calls. Related open work is #45 (batch price history) which would reduce the same fan-out.
Notes
docs/SEARCH.md§9 records that the Spring backend has no search at all, so there is no autocomplete or typeahead endpoint to lean on, ours or upstream. Upstream/v1/productstakesq,fuzzyandlimitonly, capped at 100 rows, with no paging.docs/SEARCH.mdneeds updating.