Skip to content

Feat/reusable search input#379

Merged
Damidesign merged 3 commits into
Navin-xmr:mainfrom
darcszn:feat/reusable-search-input
Jun 26, 2026
Merged

Feat/reusable search input#379
Damidesign merged 3 commits into
Navin-xmr:mainfrom
darcszn:feat/reusable-search-input

Conversation

@darcszn

@darcszn darcszn commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary
Shipments, Notifications, and Team Management each had their own inline search input with inconsistent debouncing and styling. This PR extracts a single shared SearchInput component and replaces the existing per-page implementations.

New Component — frontend/src/components/ui/SearchInput.tsx
Props
Prop Type Default Description
value string — Controlled value from the parent
onChange (value: string) => void — Called after the debounce delay
placeholder string 'Search...' Input placeholder text
debounceMs number 300 Milliseconds to wait before firing onChange
isLoading boolean false Shows a spinner in place of the clear button
Behaviour
Debounce — internal inputValue state is updated on every keystroke (so the input feels instant), but onChange is only called after the user pauses for debounceMs ms via useEffect + clearTimeout.
No double-fire on mount — a hasMounted ref skips the debounce effect on the initial render, preventing a redundant onChange call when the parent already handles its own initial fetch.
Stable callback — onChangeRef stores the latest onChange without putting it in the debounce effect's dependency array, so the timer is never unnecessarily reset by an unstable parent callback.
External sync — a separate useEffect([value]) keeps inputValue in sync when the parent resets the value programmatically (e.g. clearing from a URL param change).
Clear button — clicking × calls onChange('') immediately (no debounce wait), updates inputValue, and refocuses the input via inputRef.
Loading spinner — when isLoading is true, Loader2 (animated spin) replaces the × button on the right side.
Icons — Search icon (left, always visible), X / Loader2 (right, conditional) all from lucide-react.
Changes — Shipments.tsx
Added searchQuery state wired to SearchInput.
filteredShipments derived with useMemo: filters the already-loaded shipments array by shipment ID, origin, and destination (case-insensitive). Because the Shipments API does not expose a server-side q parameter, filtering is done client-side against the in-memory page.
The virtualiser now receives filteredShipments instead of shipments so virtual row indices stay accurate during a search.
hasMore still derives from the raw shipments vs total, so infinite-scroll load-more continues to work even while a filter is active.
Added an isFilterEmpty guard that renders a "No results found" empty state when the query matches nothing in the loaded set.
Summary line updates to show N of M loaded when a filter is active, vs N of total when no filter is set.
Changes — NotificationsPage.tsx
Replaced the inline

+ + block with .
Removed the now-unused handleSearchChange handler ((ChangeEvent) => void).
Removed the Search named import from lucide-react (handled inside SearchInput).
Removed the ChangeEvent import from react (no longer needed).
Passes isLoading to SearchInput so the spinner appears while the API is fetching filtered results — giving users clear feedback that their search term is being processed.
setSearchQuery is passed directly as onChange; because SearchInput debounces internally, the parent's searchQuery state (and therefore the API useEffect) only updates after the user pauses typing.
Test Plan
Type into the Shipments search — results filter after ~300 ms, not on every keystroke
Clear via × in Shipments — all shipments reappear and input is focused
Scroll to bottom of Shipments while a filter is active — load-more still triggers
Type into the Notifications search — API request fires after debounce, spinner shows during fetch
Clear via × in Notifications — query resets immediately, full list reloads
Pass isLoading={true} to SearchInput directly — spinner renders, × does not
Programmatically reset value from outside — input updates without triggering a debounced onChange

closes #308

Emmanuelluxury and others added 3 commits June 25, 2026 23:26
…ication

- New 3-step wizard at /register/company (CompanyRegister.tsx)
  - Step 1: company name, industry dropdown, country, company size
  - Step 2: admin full name, business email, password with strength meter, confirm password
  - Step 3: review summary with per-section Edit links back to step 1/2
- Email verification confirmation page at /register/verify-email (EmailVerification.tsx)
  - Displays the registered email address passed via router state
  - Links back to login and to re-try registration
- POST /auth/register/company API call on final submit; no token stored (email verification required first)
- CompanyRegisterRequest interface and authApi.registerCompany() added to auth endpoints
- "Create Company Account" link added to login page footer alongside existing Sign Up link
Introduces a shared SearchInput component to replace inconsistent
per-page search implementations in Shipments and NotificationsPage.

- SearchInput debounces onChange by a configurable delay (default 300ms)
  using useEffect + clearTimeout, so the parent is only notified after
  the user pauses typing
- A hasMounted guard prevents onChange from firing on initial render,
  avoiding duplicate API calls on page load
- onChangeRef keeps the debounce effect stable regardless of whether
  the parent memoises the callback
- Clear button (X) resets the value immediately (bypassing debounce)
  and refocuses the input
- Spinner (Loader2) is rendered on the right when isLoading is true,
  otherwise X is shown when the input is non-empty

Shipments.tsx:
- Adds a searchQuery state wired to SearchInput
- Filters loaded shipments client-side (id, origin, destination,
  case-insensitive) via a useMemo-derived filteredShipments list
- Passes filteredShipments to the virtualiser so virtual rows stay
  accurate during a search
- Shows a "No results found" empty state when the query matches nothing
- Updates the summary line to reflect filtered vs loaded/total counts

NotificationsPage.tsx:
- Replaces the inline Search icon + raw <input> block with SearchInput
- Removes the now-unnecessary handleSearchChange handler and the
  direct Search lucide import
- Passes isLoading so the spinner appears while the API fetches results
@Damidesign
Damidesign merged commit e36ec57 into Navin-xmr:main Jun 26, 2026
2 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants