Skip to main content

C8 — Search & Filter Patterns

Scope: Standard search and filter UX patterns used across all Ghasi Melmastoon surfaces. Covers the consumer meta-search, in-app list filtering (arrivals board, housekeeping board, folio list, report filter), and mobile search affordances.

State management: All filter state lives in URL (via nuqs on web / React Navigation params on mobile). Search results are server state managed by TanStack Query.


1.1 Search bar anatomy

The primary search input on the consumer meta layer (web + mobile):

[Destination] [Check-in] [Check-out] [Guests] [Search]
  • Destination field: Free-text with autocomplete. Suggestions from search-aggregation-service (city, district, property name). Debounce 300 ms.
  • Date range picker: <DateRangePicker> from @ghasi/ui-melmastoon. Shows Gregorian + Hijri (configurable by locale). Block past dates.
  • Guests picker: Compact popover — adult/child count stepper. Min 1 adult. Max per room type (from property config), surfaced as a soft limit.
  • Search button: Submits; disables during fetch; shows L-02 inline spinner.

URL state (nuqs):

?dest=Kabul&from=2026-05-01&to=2026-05-03&adults=2&children=0&rooms=1

1.2 Filter panel

Appears below the search bar on results view (collapsed by default on mobile).

FilterTypeValuesURL param
Price rangeRange slider (dual thumb)0 – 99,999 AFN per nightprice_min, price_max
Star ratingMulti-select chips1★ to 5★stars (comma-separated)
AmenitiesMulti-select checkboxesHalal kitchen, Prayer room, Parking, Pool, Gym, Wi-Fi, AC, Elevator, Family rooms, Pet-friendlyamenities (comma-separated)
Property typeMulti-select chipsGuesthouse, Hotel, Serviced apartment, Hostelprop_type
CancellationSingle selectFree cancellation, Non-refundable, Bothcancel
Payment methodMulti-select chipsCash on arrival, Card, Mobile moneypayment
Language spokenMulti-select checkboxesPashto, Dari, English, Uzbek, Arabic, Tajiklang

Filter behavior:

  • Each filter change triggers an immediate re-search (debounced 150 ms for sliders).
  • Active filters shown as dismissible chips above the results list.
  • "Clear all filters" button appears when any filter is active.
  • Filter count badge on mobile filter toggle button.

1.3 Sort order

SortURL valueDefault
Price (low to high)price_asc
Price (high to low)price_desc
Distance from centerdistance✅ Default for city searches
Guest ratingrating_desc
PopularitypopularityDefault for "trending"

Sort select exposed as a <Select> above the results list (desktop) or bottom sheet (mobile).

  • Map view: Leaflet + OpenStreetMap tiles.
  • "Search this area" button appears when user pans/zooms; triggers a bounding-box search (bbox query param).
  • bbox format: lng_min,lat_min,lng_max,lat_max.
  • Active bbox shown as a dotted rectangle overlay on the map.
  • Pins cluster when > 20 in view (leaflet.markercluster).
  • Pin tooltip: property name + min rate.

2. Backoffice list search (operator desktop)

2.1 Arrivals board search / filter

FilterTypeBehavior
Guest nameText searchClient-side filter on loaded data, debounce 200 ms
Room numberText / numericExact match
Arrival dateDate pickerFilter by check-in date; default: today
Reservation statusMulti-select chipsConfirmed, Checked-in, No-show, Cancelled
Payment methodMulti-selectCash, Card, Cash deposit

Results update in real-time from the local SQLite query as the user types.

2.2 Housekeeping board search / filter

FilterType
Room numberText
FloorSelect
StatusMulti-select chips (Dirty / Cleaning / Clean / OOO / OOS)
Assigned toStaff select
PriorityHigh / Normal / Low chips

Housekeeping board is not paginated — all rooms for the property load in one SQLite query (< 500 rooms per property).

FilterType
Date rangeDate range picker
Charge categoryMulti-select
Amount rangeRange slider
StatusMulti-select (Posted / Voided / Settled)

Paginated: 50 per page, cursor-based, client-side pagination on local SQLite.


3. Mobile search patterns

On mobile (consumer and staff), search is a full-screen bottom sheet:

  • Search bar at top.
  • Below: recent searches (last 5), then suggestions.
  • On input: autocomplete results replace suggestions.
  • Submit via keyboard "Go" / "Search" action.

3.2 Filter bottom sheet

  • Triggered by "Filter" chip at top of results.
  • Full-height bottom sheet with filter groups.
  • "Apply" button at bottom applies all filters atomically.
  • "Reset" button clears all filters.

4. Search result rendering

4.1 Property card (consumer)

[Photo]
[Property name]
[Rating • Star count]
[Amenity icons row]
[From AFN X,XXX / night]
[Availability summary]
[Book / View →]
  • Photos: lazy-loaded with LQIP placeholder.
  • Availability summary: "X rooms left" or "Available" (not exact inventory — privacy).
  • Rate: lowest available rate for the search period; "From" prefix.
  • RTL: card layout flips (photo on right, text on left in RTL).

4.2 Map pin

[AFN X,XXX] ← clickable
  • Selected pin expands to a popover with mini card.
  • Popover contains: name, rating, min rate, "View" link.

4.3 Pagination

  • Web: infinite scroll with IntersectionObserver + React Query fetchNextPage.
  • Mobile: FlatList with onEndReached.
  • Max 200 results per query (enforced by search-aggregation-service).
  • "Load more" button as fallback for users who prefer it.

5. Telemetry

EventWhenKey properties
mel.consumer.search.executedSearch submitteddestination, dates, result_count, latency_ms
mel.consumer.search.filter_appliedFilter added/removedfilter_key, filter_value, result_count_after
mel.consumer.search.sort_changedSort changedsort_key, result_count
mel.consumer.search.no_results0 resultsdestination, filter_count

Full schema in C1 — Telemetry Event Dictionary §2.1.


6. Accessibility

  • All filter controls are keyboard-navigable.
  • Active filter chips have a visible ✕ button with aria-label="Remove {filter_name} filter".
  • "Clear all filters" is a visible button, not hidden behind a dropdown.
  • Range sliders: two-thumb with ARIA role="slider", aria-valuemin, aria-valuemax, aria-valuenow.
  • Map view: keyboard-navigable pins (tabindex=0, role="button" on each marker), aria-label="<property name>, AFN X,XXX per night".
  • Map is not the only way to discover properties (list view always available).

7. Open Questions

  • Price range filter: should the slider use AFN (local currency) or the user's preferred display currency? Currently AFN only.
  • Guest-language filter: should it filter on property.staffLanguages (languages the staff speak) or property.amenities.languages (languages the property supports overall)?
  • Arrivals board: should name search be fuzzy (trigram similarity) or exact prefix? Fuzzy adds SQLite FTS5 requirement.

References