Location Finder
Loading playground
Overview
LocationFinder is the composite store-locator. It pairs a sidebar (heading, search input with autocomplete, optional filter sheet, scrollable list) with a Google Maps view of the same locations. The layout shifts between two responsive modes:
- Desktop (
>= --lg): sidebar and map render side-by-side. Selecting a row in the list re-centers the map and pops a Google MapsInfoWindowover the marker. - Mobile (
< --lg): sidebar and map become two tabs (List/Map). Selecting a row activates the map tab; the selected store renders as aLocationCardbottom sheet over the map (noInfoWindow).
Search filters by name and address. Filters work via the same AvailableFilter / SelectedFilters shape used by FilterPanelContent. Both are state-only: the parent owns the data and the filter definitions.
LocationFinder composes LocationFinderList and LocationFinderMap internally. Both are exported from @laioutr-core/ui so you can drop them in independently when you need just the list or just the map.
The containerStyle prop ('full-width' | 'boxed', via the shared containerStyleField toggle in Studio) controls the desktop framing. In boxed mode, .location-finder--boxed clamps the finder to --container-max-width and centers it with margin-inline: auto from the --lg breakpoint up, so it sits as a contained card rather than stretching edge-to-edge.
Google Maps setup
The map needs a Google Maps JavaScript API key and a Map ID.
In a Studio storefront the key is one option on the @laioutr-app/ui module. The Location Finder and Location Detail sections read it and pass the loaded API down.
export default defineNuxtConfig({
modules: ['@laioutr-app/ui'],
'@laioutr-app/ui': {
googleMapsApiKey: process.env.GOOGLE_MAPS_API_KEY,
},
});
The Map ID is a per-section field in Studio ("Google Maps Map ID"). Advanced Markers need one. DEMO_MAP_ID works while you develop.
The Maps script loads on the first page that shows a map, not on every page. Without a key the section keeps its search, list and filters, the map area stays empty, and the browser console names the missing option.
Outside Studio, when you render LocationFinder yourself, pass apiPromise and mapId as props. apiPromise resolves to the loaded google namespace.
Key Business & UX Benefits
- Search-by-name and search-by-address from the same input means shoppers can find a store by whatever they remember (the brand of the location, the street it sits on, or the neighborhood).
- The Google Maps view with marker selection lets shoppers visually scan a region and tap the closest store, lifting "find a store" conversion on mobile where typing an address is friction.
- Filter sheet shares the
AvailableFilter/SelectedFiltersshape withFilterOffCanvas, so the same filter UI works on a category page and a store finder without per-surface forks. - Tabbed mobile layout keeps the map full-bleed when needed, then collapses cleanly into a list scroll for shoppers who prefer to read text over panning.
API Reference
LocationFinder
| Prop | Default | Type |
|---|---|---|
locationsrequired | LocationFinderMapItem[] { image, id, name, 9 more } | |
apiPromiserequired | GoogleMapsApiPromise | |
mapIdrequired | string | |
heading | string | |
containerStyle | full-width | "full-width" | "boxed" |
suggestions | [] | InputAutocompleteOption[] { value, label, location, 3 more } |
filters | [] | |
filteredCount | number | |
userLocation | { lat, lng } | |
defaultCenter | { lat, lng } | |
defaultZoom | number | |
selectedZoom | number | |
isLoading | false | boolean |
selectedLocationId | string | |
searchQuery | string | |
selectedFilters | object | |
view | "list" | "map" |
| Slot | Type |
|---|---|
empty | {} |
| Event | Type |
|---|---|
details | (event: "details", id: string): void |
navigate | (event: "navigate", id: string): void |
call | (event: "call", id: string): void |
update:selectedLocationId | (event: "update:selectedLocationId", value: string | undefined): void |
update:searchQuery | (event: "update:searchQuery", value: string): void |
update:selectedFilters | (event: "update:selectedFilters", value: SelectedFilters): void |
update:view | (event: "update:view", value: "list" | "map"): void |
The component takes a locations: LocationFinderMapItem[] array (the shape exported from LocationFinderMap), a Google Maps apiPromise and mapId (see Google Maps setup), and a set of optional v-models:
v-model:selectedLocationId: the currently focused store; emitted on row click or marker click.v-model:searchQuery: the text in the search input.v-model:selectedFilters: the active filter selections.v-model:view:'list'or'map'. Only meaningful at mobile breakpoints; ignored at desktop.
Event emits (navigate, call, details) all carry the location id. They forward unchanged from the inner LocationCard / map InfoWindow / list items, so the parent handles routing in one place.
LocationFinderList
Standalone list of locations. Use it when you need a list without the map (e.g. a printable directory page, an admin overview).
| Prop | Default | Type |
|---|---|---|
locationsrequired | LocationFinderListItem[] | |
selectedLocationId | string |
| Event | Type |
|---|---|
details | (event: "details", id: string): void |
navigate | (event: "navigate", id: string): void |
call | (event: "call", id: string): void |
selectLocation | (event: "selectLocation", id: string): void |
Each row renders a LocationCard in the list variant. The list emits select-location (highlight without navigation), navigate, call, and details.
LocationFinderMap
Standalone Google Maps view. Use it when you need a map without the surrounding chrome.
| Prop | Default | Type |
|---|---|---|
locationsrequired | LocationFinderMapItem[] { image, id, name, 9 more } | |
apiPromiserequired | GoogleMapsApiPromiseResolved by the storefront Nuxt plugin (Google Maps JS API). | |
mapIdrequired | string | |
selectedLocationId | string | |
userLocation | { lat, lng } | |
defaultCenter | { lat, lng } | |
defaultZoom | 12 | number |
selectedZoom | 15 | number |
noPopup | false | boolean |
| Event | Type |
|---|---|
details | (event: "details", id: string): void |
navigate | (event: "navigate", id: string): void |
call | (event: "call", id: string): void |
selectLocation | (event: "selectLocation", id: string): void |
Requires a Google Maps apiPromise (a resolved promise of the loaded google.maps namespace) and a mapId (your Google Cloud Map Style ID) — see Google Maps setup. The no-popup prop suppresses the InfoWindow overlay. Set it on mobile when a bottom-sheet card handles the selection UI instead.
Composition
The default desktop layout is roughly:
+--------------------------------+--------------------+
| heading | |
| search input | |
| [List | Map] [Filter] | |
| ------------------------------ | map |
| <LocationCard> | |
| <LocationCard> | |
| <LocationCard> | |
| ... | |
+--------------------------------+--------------------+
The sidebar width is themable via --location-finder-sidebar-width (default 414px), and the maximum desktop height via --location-finder-max-height-lg (default 740px).
Slots
#empty: replaces the defaultEmptyStatewhen no locations match the search/filter combination. Use this to surface a brand-tone empty state ("No bakeries within 25 km. Try expanding the radius.") in place of the generic message.
Related
LocationCard: the card rendered for each result row and inside the mobile bottom sheet.LocationDetail: the per-store detail page this component links to viadetails.OpeningHourstype: the shape each location's opening data takes.
Changelog
The container-style control moved to the shared containerStyleField toggle ('full-width' | 'boxed'); the field name and stored values are unchanged, so no re-authoring is needed. In boxed mode .location-finder--boxed now clamps to --container-max-width and centres with margin-inline: auto from the --lg breakpoint up.
Location Detail
Single-location detail layout with a top-of-page Google Maps view and a slot for header, info list, and any other store-detail content.
Table Opening Hours
Weekly opening-hours schedule rendered as a `BasicTable`. Groups consecutive same-hours days into a single row (e.g. "Monday - Wednesday 09:00 - 18:00").