Skip to content

Commit

Permalink
AutocompleteJS v1.0 | Sales locations pages (#1841)
Browse files Browse the repository at this point in the history
* chore: use .heex extension

* feat(PlacesController): find more locations & URLs

* refactor(autocomplete): update location fetching

use new backend endpoints, make desired URL configurable

* feat(autocomplete): popular locations plugin

* fix popular location icons for North Station

* working wip: update search inputs

* feat(algolia_autocomplete): make submit handler configurable

* feat(autocomplete): populate initial state
  • Loading branch information
thecristen authored Jan 2, 2024
1 parent 6f9bb76 commit e96d836
Show file tree
Hide file tree
Showing 27 changed files with 656 additions and 220 deletions.
7 changes: 6 additions & 1 deletion apps/location_service/lib/aws_location/aws_location.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,14 @@ defmodule AWSLocation do
|> Result.handle_response([latitude, longitude])
end

@spec autocomplete(String.t(), number) :: LocationService.Suggestion.result()
@spec autocomplete(String.t(), number) ::
LocationService.Suggestion.result()
| {:error, :invalid_arguments}
| {:error, :zero_results}
def autocomplete(search, limit) when 1 <= limit and limit <= 15 do
Request.autocomplete(search, limit)
|> Result.handle_response(%{search: search, limit: limit})
end

def autocomplete(_, _), do: {:error, :invalid_arguments}
end
29 changes: 27 additions & 2 deletions apps/site/assets/css/_autocomplete-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,12 @@
padding-right: var(--aa-spacing-half);
}
}

#header-mobile {
@extend %shared-autocomplete;
}

#error-page {
%large-autocomplete {
--aa-search-input-height: 2.5rem;

@extend %shared-autocomplete;
Expand All @@ -165,3 +165,28 @@
transform: scale(-1, 1);
}
}

#error-page {
@extend %large-autocomplete;
}

#proposed-sales-locations,
#sales-locations {
@extend %large-autocomplete;

.c-search-bar__autocomplete-results {
position: relative;

// Autocomplete.JS doesn't support multiple instances per page, and one way
// this manifests is it totally bungles the dynamic positioning of elements.
// Hence the need to wrangle with !important.
/* stylelint-disable declaration-no-important */
.aa-Panel {
left: 0 !important;
margin-top: .25rem;
top: 0 !important;
width: 100% !important;
}
/* stylelint-enable declaration-no-important */
}
}
2 changes: 1 addition & 1 deletion apps/site/assets/js/algolia-result.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ function _iconFromRoute(route) {
}
}

function getPopularIcon(icon) {
export function getPopularIcon(icon) {
switch (icon) {
case "airplane":
return TEMPLATES.fontAwesomeIcon.render({ icon: "fa-plane" });
Expand Down
4 changes: 2 additions & 2 deletions apps/site/assets/js/trip-planner-location-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -494,8 +494,8 @@ TripPlannerLocControls.POPULAR = [
name: "North Station",
features: [
"orange_line",
"green-line-c",
"green-line-e",
"green_line_c",
"green_line_e",
"bus",
"commuter_rail",
"access"
Expand Down
10 changes: 0 additions & 10 deletions apps/site/assets/ts/ui/__tests__/autocomplete-helpers-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
} from "../autocomplete/__autocomplete";
import {
getTitleAttribute,
transitNearMeURL,
STATE_CHANGE_HANDLERS
} from "./../autocomplete/helpers";
import * as MediaBreakpoints from "../../helpers/media-breakpoints";
Expand Down Expand Up @@ -43,15 +42,6 @@ test("getTitleAttribute indicates name to highlight", () => {
]);
});

test("transitNearMeURL maps a lat/lon to a URL", () => {
const urlNoParams = transitNearMeURL(1, 2);
expect(urlNoParams).toEqual("/transit-near-me?latitude=1&longitude=2");
const urlWithParams = transitNearMeURL(1, 2, "other=params,go,here");
expect(urlWithParams).toEqual(
"/transit-near-me?latitude=1&longitude=2&other=params,go,here"
);
});

describe("onStateChange handlers - nav", () => {
const onStateChange = STATE_CHANGE_HANDLERS["nav"];

Expand Down
171 changes: 124 additions & 47 deletions apps/site/assets/ts/ui/__tests__/autocomplete-plugins-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import createGeolocationPlugin from "../autocomplete/plugins/geolocation";
import {
AutocompleteItem,
Item,
LocationItem
LocationItem,
PopularItem
} from "../autocomplete/__autocomplete";
import createPopularLocationsPlugin from "../autocomplete/plugins/popular";

const mockAlgoliaResponse = {
results: [
Expand All @@ -32,20 +34,42 @@ const mockAlgoliaResponse = {
] as SearchResponse[]
};

const mockLocationPredictions = {
predictions: JSON.stringify([
{ address: "123 Sesame St", highlighted_spans: {} },
{ address: "50 Wisteria Lane", highlighted_spans: {} },
{ address: "1600 Pennsylvania Ave", highlighted_spans: {} }
])
};

const mockAddressDetails = {
result: JSON.stringify({
latitude: 1,
longitude: 2,
formatted: ""
})
const mockLocations = {
result: [
{
address: "123 Sesame St",
highlighted_spans: {},
latitude: 1,
longitude: 2,
urls: {
"transit-near-me": "/transit-near-me/1/2",
"retail-sales-locations": "/retail/1/2",
"proposed-sales-locations": "/proposed/1/2"
}
},
{
address: "50 Wisteria Lane",
highlighted_spans: {},
latitude: 3,
longitude: 4,
urls: {
"transit-near-me": "/transit-near-me/3/4",
"retail-sales-locations": "/retail/3/4",
"proposed-sales-locations": "/proposed/3/4"
}
},
{
address: "1600 Pennsylvania Ave",
highlighted_spans: {},
latitude: 5,
longitude: 6,
urls: {
"transit-near-me": "/transit-near-me/5/6",
"retail-sales-locations": "/retail/5/6",
"proposed-sales-locations": "/proposed/5/6"
}
}
]
};

const mockFetch = (returnedData: unknown) => () =>
Expand Down Expand Up @@ -114,47 +138,73 @@ describe("Algolia v1 plugins", () => {
});

describe("locations", () => {
beforeEach(() => {
window.fetch = jest
.fn()
.mockImplementationOnce(mockFetch(mockLocationPredictions))
.mockImplementation(mockFetch(mockAddressDetails));
beforeAll(() => {
window.fetch = jest.fn().mockImplementation(mockFetch(mockLocations));
});

test("does nothing without a query", () => {
const params = {} as GetSourcesParams<Item>;
const { getSources } = createLocationsPlugin();
const { getSources } = createLocationsPlugin(3);
expect(getSources!(params)).toEqual([]);
});

test("makes GET request to backend", async () => {
const params = {
query: "some area"
} as GetSourcesParams<Item>;
const { getSources } = createLocationsPlugin();
const { getSources } = createLocationsPlugin(3);
const sources = (await getSources!(params)) as AutocompleteSource<Item>[];
await sources[0].getItems(params);
expect(window.fetch).toHaveBeenCalledWith(
"/places/autocomplete/some%20area/2/null"
);
expect(window.fetch).toHaveBeenCalledWith("/places/search/some%20area/3");
});

test("returns results with URLs to transit near me", async () => {
test("returns results with URLs to chosen URL type", async () => {
const params = {
query: "this place"
} as GetSourcesParams<Item>;
const { getSources } = createLocationsPlugin();
const { getSources } = createLocationsPlugin(3, "transit-near-me");
const sources = (await getSources!(params)) as AutocompleteSource<Item>[];
const response = await sources[0].getItems(params);
const urls = (response as LocationItem[]).map(r => r.url);
urls.forEach(u => {
expect(u).toStartWith("/transit-near-me?");
expect(u).toContain("&query=this%20place");
(response as LocationItem[]).forEach(location => {
expect(location.url).toContain("/transit-near-me");
});
});
});

describe("geolocation", () => {
beforeAll(() => {
window.fetch = jest.fn().mockImplementation(
mockFetch({
result: {
address: "51.1, 45.3",
highlighted_spans: {},
latitude: 51.1,
longitude: 45.3,
urls: {
"transit-near-me": "/transit-near-me/51.1/45.3",
"retail-sales-locations": "/retail/51.1/45.3",
"proposed-sales-locations": "/proposed/51.1/45.3"
}
}
})
);

const mockGeolocation = {
getCurrentPosition: jest.fn().mockImplementationOnce(success =>
Promise.resolve(
success({
coords: {
latitude: 51.1,
longitude: 45.3
}
})
)
)
};
(global as any).navigator.geolocation = mockGeolocation;
(global as any).Turbolinks = { visit: jest.fn() };
});

test("return source defining template", async () => {
const templateItem = {} as LocationItem;
const params = {
Expand Down Expand Up @@ -196,7 +246,7 @@ describe("Algolia v1 plugins", () => {
const params = {
setIsOpen: value => {}
} as GetSourcesParams<Item>;
const { getSources } = createGeolocationPlugin();
const { getSources } = createGeolocationPlugin("retail-sales-locations");
const sources = (await getSources!(params)) as AutocompleteSource<Item>[];
const itemTemplate = sources[0].templates.item as SourceTemplates<
Item
Expand All @@ -211,29 +261,56 @@ describe("Algolia v1 plugins", () => {
} as AutocompleteState<Item>
})
);
const mockGeolocation = {
getCurrentPosition: jest.fn().mockImplementationOnce(success =>
Promise.resolve(
success({
coords: {
latitude: 51.1,
longitude: 45.3
}
})
)
)
};
(global as any).navigator.geolocation = mockGeolocation;
(global as any).Turbolinks = { visit: jest.fn() };
fireEvent.click(screen.getByRole("button"));
await waitFor(() => {
expect(
window.navigator.geolocation.getCurrentPosition
).toHaveBeenCalled();
expect(window.Turbolinks.visit).toHaveBeenCalledWith(
"/transit-near-me?latitude=51.1&longitude=45.3"
"/retail/51.1/45.3"
);
});
});
});

describe("popular locations", () => {
test("doesn't return source if there's a query", async () => {
const params = {
query: "looking for something"
} as GetSourcesParams<Item>;
const { getSources } = createPopularLocationsPlugin();
const sources = (await getSources!(params)) as AutocompleteSource<Item>[];
expect(sources).toEqual([]);
});

test("returns results with URLs to chosen URL type", async () => {
window.fetch = jest.fn().mockImplementation(
mockFetch({
result: [
{
icon: "station",
name: "South Station",
features: ["red_line", "bus", "commuter_rail", "access"],
latitude: 42.352271,
longitude: -71.055242,
urls: {
"transit-near-me": "/transit-near-me/42,-71",
"retail-sales-locations": "/retail-sales-locations/42,-71",
"proposed-sales-locations": "/proposed-sales-locations/42,-71"
}
}
]
})
);
const params = {} as GetSourcesParams<Item>;
const { getSources } = createPopularLocationsPlugin(
"proposed-sales-locations"
);
const sources = (await getSources!(params)) as AutocompleteSource<Item>[];
const response = await sources[0].getItems(params);
(response as PopularItem[]).forEach(location => {
expect(location.url).toContain("/proposed-sales-locations");
});
});
});
});
9 changes: 7 additions & 2 deletions apps/site/assets/ts/ui/autocomplete/__autocomplete.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Hit } from "@algolia/client-search";
import { BaseItem } from "@algolia/autocomplete-core";
import { Route, RouteType, Stop } from "../../__v3api";
import { HighlightedSpan } from "../../helpers/text";
import { TripPlannerLocControls } from "../../../js/trip-planner-location-controls";

type AlgoliaItem = Hit<{ index: string }> & BaseItem;

Expand All @@ -28,10 +29,14 @@ type ContentItem = {
} & AlgoliaItem;

export type LocationItem = {
highlighted_spans: HighlightedSpan[];
longitude: number;
latitude: number;
address: string;
highlighted_spans: HighlightedSpan[];
url: string;
};

export type PopularItem = typeof TripPlannerLocControls.POPULAR[number];

export type AutocompleteItem = RouteItem | StopItem | ContentItem;
export type Item = AutocompleteItem | LocationItem;
export type Item = AutocompleteItem | LocationItem | PopularItem;
Loading

0 comments on commit e96d836

Please sign in to comment.