Skip to content

Commit

Permalink
Groundwork for search by location (#2158)
Browse files Browse the repository at this point in the history
* chore: models for location result data and results

* chore: make API call to get location search results

* fix: correct return format of search and suggest functions

* test: factories for location result

* feat: useLoationSearchResults hook

* feat: provide logic to separate out location results

* refactor: change argument type of useLocationSearchResults

* feat: basic list display of location search results

* feat: put location search results in cards

* test: basic tests for showing locations in new search

* refactor: make way for selecting locations

* refactor: remove unneeded single underscore variable

* feat: locations can be selected entities, not shown on map yet

* feat: parse ID into location object on frontend

* test: ensure show more button doesn't show up with <= 5 results

* refactor: better naming of selection handler callbacks
  • Loading branch information
lemald authored Jul 28, 2023
1 parent 2deece0 commit ba0462e
Show file tree
Hide file tree
Showing 16 changed files with 568 additions and 38 deletions.
15 changes: 15 additions & 0 deletions assets/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ import {
routePatternsFromData,
} from "./models/routePatternData"
import * as Sentry from "@sentry/react"
import { LocationSearchResult } from "./models/locationSearchResult"
import {
LocationSearchResultData,
locationSearchResultsFromData,
} from "./models/locationSearchResultData"

export interface RouteData {
id: string
Expand Down Expand Up @@ -212,6 +217,16 @@ export const fetchSwings = (routeIds: RouteId[]): Promise<Swing[] | null> =>
defaultResult: [],
})

export const fetchLocationSearchResults = (
searchText: string
): Promise<LocationSearchResult[] | null> =>
checkedApiCall<LocationSearchResultData[], LocationSearchResult[] | null>({
url: `api/location_search/search?query=${searchText}`,
dataStruct: array(LocationSearchResultData),
parser: nullableParser(locationSearchResultsFromData),
defaultResult: [],
})

export const putNotificationReadState = (
newReadState: NotificationState,
notificationIds: NotificationId[]
Expand Down
58 changes: 41 additions & 17 deletions assets/src/components/mapPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import OldSearchForm from "./oldSearchForm"
import inTestGroup, { TestGroups } from "../userInTestGroup"
import { Socket } from "phoenix"
import SearchResultsByProperty from "./mapPage/searchResultsByProperty"
import { LocationSearchResult } from "../models/locationSearchResult"

const thereIsAnActiveSearch = (
vehicles: (Vehicle | Ghost)[] | null,
Expand Down Expand Up @@ -69,18 +70,16 @@ const OldSearchResultList = ({
}

const SearchMode = ({
selectSearchResult,
onSelectVehicleResult,
onSelectLocationResult,
}: {
selectSearchResult: (result: Vehicle | Ghost | null) => void
onSelectVehicleResult: (result: Vehicle | Ghost | null) => void
onSelectLocationResult: (result: LocationSearchResult | null) => void
}): React.ReactElement => {
const CurrentSearchForm = inTestGroup(TestGroups.LocationSearch)
? SearchFormFromStateDispatchContext
: OldSearchForm

const CurrentSearchResults = inTestGroup(TestGroups.LocationSearch)
? SearchResultsByProperty
: OldSearchResultList

const [{ searchPageState }] = useContext(StateDispatchContext)
return (
<>
Expand All @@ -98,7 +97,14 @@ const SearchMode = ({

<div className="c-search-display u-hideable">
{searchPageState.isActive ? (
<CurrentSearchResults selectSearchResult={selectSearchResult} />
inTestGroup(TestGroups.LocationSearch) ? (
<SearchResultsByProperty
onSelectVehicleResult={onSelectVehicleResult}
onSelectLocationResult={onSelectLocationResult}
/>
) : (
<OldSearchResultList selectSearchResult={onSelectVehicleResult} />
)
) : (
<RecentSearches />
)}
Expand Down Expand Up @@ -221,12 +227,12 @@ const Selection = ({
vehicleId={selectedEntity.vehicleId}
setSelection={setSelection}
/>
) : (
) : selectedEntity.type === SelectedEntityType.RoutePattern ? (
<SelectedRoute
selectedRoutePattern={selectedEntity}
selectRoutePattern={selectRoutePattern}
/>
)}
) : null}
</div>
)
}
Expand All @@ -251,7 +257,7 @@ const MapPage = (): ReactElement<HTMLDivElement> => {
)
// #endregion

const setSelection = useCallback(
const setVehicleSelection = useCallback(
(selectedEntity: SelectedEntity | null) => {
switch (selectedEntity?.type) {
case SelectedEntityType.Vehicle:
Expand All @@ -269,20 +275,35 @@ const MapPage = (): ReactElement<HTMLDivElement> => {
[dispatch]
)

const selectSearchResult = useCallback(
const selectVehicleResult = useCallback(
(vehicleOrGhost: Vehicle | Ghost | null) => {
if (vehicleOrGhost) {
setSelection({
setVehicleSelection({
type: SelectedEntityType.Vehicle,
vehicleId: vehicleOrGhost.id,
})
} else {
setSelection(null)
setVehicleSelection(null)
}
},
[setSelection]
[setVehicleSelection]
)

const selectLocationResult = (
location: LocationSearchResult | null
): void => {
if (location) {
dispatch(
setSelectedEntity({
type: SelectedEntityType.Location,
location,
})
)
} else {
dispatch(setSelectedEntity(null))
}
}

return (
<div
className="c-map-page inherit-box border-box"
Expand All @@ -304,16 +325,19 @@ const MapPage = (): ReactElement<HTMLDivElement> => {
{selectedEntity ? (
<Selection
selectedEntity={selectedEntity}
setSelection={setSelection}
setSelection={setVehicleSelection}
/>
) : (
<SearchMode selectSearchResult={selectSearchResult} />
<SearchMode
onSelectVehicleResult={selectVehicleResult}
onSelectLocationResult={selectLocationResult}
/>
)}
</div>
<div className="c-map-page__map">
<MapDisplay
selectedEntity={selectedEntity}
setSelection={setSelection}
setSelection={setVehicleSelection}
/>
</div>
</div>
Expand Down
103 changes: 95 additions & 8 deletions assets/src/components/mapPage/searchResultsByProperty.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,36 @@ import { setPropertyMatchLimit } from "../../state/searchPageState"
import Loading from "../loading"
import SearchResults from "../searchResults"
import React from "react"
import { useLocationSearchResults } from "../../hooks/useLocationSearchResults"
import { Card, CardBody } from "../card"
import { LocationSearchResult } from "../../models/locationSearchResult"

const SearchResultSection = ({
const SearchResultSection = (props: {
property: SearchProperty
text: string
limit: number
onSelectVehicle: (vehicle: Vehicle | Ghost) => void
onSelectLocation: (location: LocationSearchResult) => void
showMore: () => void
}) => {
if (props.property === "location") {
return <LocationSearchResultSection {...props} />
} else {
return <VehicleSearchResultSection {...props} />
}
}

const VehicleSearchResultSection = ({
property,
text,
limit,
selectVehicle,
onSelectVehicle,
showMore,
}: {
property: SearchProperty
text: string
limit: number
selectVehicle: (vehicle: Vehicle | Ghost) => void
onSelectVehicle: (vehicle: Vehicle | Ghost) => void
showMore: () => void
}) => {
const { socket } = useContext(SocketContext)
Expand Down Expand Up @@ -54,7 +72,7 @@ const SearchResultSection = ({
<SearchResults
vehicles={limitedSearchResults.matchingVehicles}
selectedVehicleId={null}
onClick={selectVehicle}
onClick={onSelectVehicle}
/>
{limitedSearchResults.hasMoreMatches && (
<div className="c-map_page__search_results_actions">
Expand All @@ -74,17 +92,85 @@ const SearchResultSection = ({
)
}

const LocationSearchResultSection = ({
text,
limit,
onSelectLocation,
showMore,
}: {
text: string
limit: number
onSelectLocation: (location: LocationSearchResult) => void
showMore: () => void
}) => {
const locationSearchResults = useLocationSearchResults(text)

return (
<section
className="c-map-page__search_results_section"
aria-labelledby={`search-results__location`}
>
<h2
className="c-map-page__search_results_header"
id={`search-results__location`}
>
{searchPropertyDisplayConfig.location.name}
</h2>
{locationSearchResults === null ? (
<Loading />
) : locationSearchResults.length > 0 ? (
<>
<ul className="c-search-results__list">
{locationSearchResults
.slice(0, limit)
.map((locationSearchResult) => (
<li key={locationSearchResult.id}>
<Card
style="white"
title={
locationSearchResult.name || locationSearchResult.address
}
openCallback={() => onSelectLocation(locationSearchResult)}
>
{locationSearchResult.name &&
locationSearchResult.address && (
<CardBody>{locationSearchResult.address}</CardBody>
)}
</Card>
</li>
))}
</ul>
{locationSearchResults.length > limit && (
<div className="c-map_page__search_results_actions">
<button
className="c-map-page__show_more button-text"
onClick={() => showMore()}
>
Show more
</button>
</div>
)}
</>
) : (
"No results found"
)}
</section>
)
}

const SearchResultsByProperty = ({
selectSearchResult,
onSelectVehicleResult,
onSelectLocationResult,
}: {
selectSearchResult: (result: Vehicle | Ghost | null) => void
onSelectVehicleResult: (result: Vehicle | Ghost | null) => void
onSelectLocationResult: (result: LocationSearchResult | null) => void
}) => {
const [{ searchPageState }, dispatch] = useContext(StateDispatchContext)

return (
<div aria-label="Grouped Search Results">
{Object.entries(searchPageState.query.properties)
.filter(([property, limit]) => limit != null && property != "location")
.filter(([, limit]) => limit != null)
.map(([property, limit]) => ({
property: property as SearchProperty,
limit: limit as number,
Expand All @@ -100,7 +186,8 @@ const SearchResultsByProperty = ({
property={property}
text={searchPageState.query.text}
limit={limit}
selectVehicle={selectSearchResult}
onSelectVehicle={onSelectVehicleResult}
onSelectLocation={onSelectLocationResult}
showMore={() =>
dispatch(setPropertyMatchLimit(property, limit + 25))
}
Expand Down
31 changes: 31 additions & 0 deletions assets/src/hooks/useLocationSearchResults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useEffect, useState } from "react"
import { fetchLocationSearchResults } from "../api"
import { LocationSearchResult } from "../models/locationSearchResult"

export const useLocationSearchResults = (
text: string | null
): LocationSearchResult[] | null => {
const [searchResults, setSearchResults] = useState<
LocationSearchResult[] | null
>(null)

useEffect(() => {
let shouldUpdate = true

if (text) {
fetchLocationSearchResults(text).then((results) => {
if (shouldUpdate) {
setSearchResults(results)
}
})
} else {
setSearchResults(null)
}

return () => {
shouldUpdate = false
}
}, [text])

return searchResults
}
7 changes: 7 additions & 0 deletions assets/src/models/locationSearchResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface LocationSearchResult {
id: string
name: string | null
address: string
latitude: number
longitude: number
}
30 changes: 30 additions & 0 deletions assets/src/models/locationSearchResultData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Infer, nullable, number, string, type } from "superstruct"
import { LocationSearchResult } from "./locationSearchResult"

export const LocationSearchResultData = type({
id: string(),
name: nullable(string()),
address: string(),
latitude: number(),
longitude: number(),
})
export type LocationSearchResultData = Infer<typeof LocationSearchResultData>

export const locationSearchResultFromData = ({
id,
name,
address,
latitude,
longitude,
}: LocationSearchResultData): LocationSearchResult => ({
id,
name,
address,
latitude,
longitude,
})

export const locationSearchResultsFromData = (
locationSearchResultsData: LocationSearchResultData[]
): LocationSearchResult[] =>
locationSearchResultsData.map(locationSearchResultFromData)
Loading

0 comments on commit ba0462e

Please sign in to comment.