Feat/reusable search input#379
Merged
Merged
Conversation
…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
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.
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
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