diff --git a/samples/stores-list-sync-map/index.njk b/samples/stores-list-sync-map/index.njk new file mode 100644 index 00000000..6731bfd9 --- /dev/null +++ b/samples/stores-list-sync-map/index.njk @@ -0,0 +1,29 @@ +{% extends '../../src/_includes/layout.njk' %} +{% block html %} + +
+ +
+
+ +{% endblock %} diff --git a/samples/stores-list-sync-map/index.ts b/samples/stores-list-sync-map/index.ts new file mode 100644 index 00000000..3e64049f --- /dev/null +++ b/samples/stores-list-sync-map/index.ts @@ -0,0 +1,259 @@ +// [START woosmap_stores_list_sync_map] +let map: woosmap.map.Map; +let activeLocation: woosmap.map.Marker | null = null; +let storesOverlay: woosmap.map.StoresOverlay; +let storesService: woosmap.map.StoresService; +let localitiesService: woosmap.map.LocalitiesService; +let visibleStores: woosmap.map.stores.StoreResponse[] = []; +let allStores: woosmap.map.stores.StoreResponse[] = []; + +const storesStyle: woosmap.map.Style = { + breakPoint: 14, + rules: [], + default: { + color: "#55baa6", + size: 10, + minSize: 3, + icon: { + url: "https://images.woosmap.com/marker-green.svg", + }, + selectedIcon: { + url: "https://images.woosmap.com/marker-red.svg", + }, + }, +}; + +const mapOptions: woosmap.map.MapOptions = { + zoom: 5, + center: { + lat: 46.5, + lng: 1.4, + }, + styles: [{ featureType: "all", stylers: [{ saturation: -100 }] }], +}; + +const inputElement = document.getElementById( + "autocomplete-input", +) as HTMLInputElement; +const submitButton = document.getElementById( + "submit-button", +) as HTMLButtonElement; + +if (inputElement && submitButton) { + inputElement.addEventListener("keydown", handleKeyPress); + submitButton.addEventListener("click", handleGeocodeFromSubmit); +} + +function handleKeyPress(event: KeyboardEvent) { + if (event.key === "Enter") { + handleGeocode(null); + } +} + +function handleGeocodeFromSubmit() { + handleGeocode(null); +} + +// Function to filter stores based on map bounds +function filterStoresByBounds( + stores: woosmap.map.stores.StoreResponse[], + bounds: woosmap.map.LatLngBounds | null, +): woosmap.map.stores.StoreResponse[] { + return stores.filter((store: woosmap.map.stores.StoreResponse) => + bounds + ? bounds.contains({ + lat: store.geometry.coordinates[1], + lng: store.geometry.coordinates[0], + }) + : false, + ); +} + +// Function to get all Stores recursively using query.page parameter +function getAllStores() { + const allStores: woosmap.map.stores.StoreResponse[] = []; + const query: woosmap.map.stores.StoresSearchRequest = { storesByPage: 300 }; + function getStores(storePage?: number) { + if (storePage) { + query.page = storePage; + } + return storesService + .search(query) + .then((response) => { + allStores.push(...response.features); + if (query.page === response.pagination.pageCount) { + return allStores; + } + return getStores(response.pagination.page + 1); + }) + .catch((err) => { + console.error(err); + throw new Error("Error getting all stores"); + }); + } + + return getStores(); +} + +function displayListStores(stores: woosmap.map.stores.StoreResponse[]) { + const storeTemplates = stores.map(createStoreTemplate); + const storeList = document.querySelector(".stores-list"); + if (storeList) { + storeList.innerHTML = `

Results: ${stores.length} stores in bounds

`; + storeList.innerHTML += storeTemplates.join(""); + } + addClickListenerToStoreCards(stores); +} + +function createStoreTemplate( + store: woosmap.map.stores.StoreResponse, + indexStore: number, +) { + const { name, address, contact } = store.properties; + const phoneHtml = contact.phone + ? `
${contact.phone}
` + : ""; + const websiteHtml = contact.website + ? `
go to website
` + : ""; + + return ` +
+
+
${name}
+
+
${ + (address && address.lines && address.lines.join(", ")) || + "Address not available" + }
+ ${phoneHtml} + ${websiteHtml} +
+
+
`; +} + +function addClickListenerToStoreCards( + stores: woosmap.map.stores.StoreResponse[], +) { + document.querySelectorAll(".summary-store-card").forEach((storeCard) => { + storeCard.addEventListener("click", () => { + storesOverlay.setSelection(stores[storeCard["dataset"].index]); + const storeList = document.querySelector(".stores-list"); + if (storeList) { + Array.from(storeList.children).forEach((child) => + child.classList.remove("active"), + ); + storeCard.classList.add("active"); + } + }); + }); +} + +function createLocalitiesRequest( + latlng: woosmap.map.LatLng | null, +): woosmap.map.localities.LocalitiesGeocodeRequest | null { + return inputElement + ? latlng + ? { latLng: latlng } + : { address: inputElement.value } + : null; +} + +function handleGeocode(latlng: woosmap.map.LatLng | null) { + const localitiesRequest = createLocalitiesRequest(latlng); + + if (localitiesRequest) { + localitiesService + .geocode(localitiesRequest) + .then((localities) => handleGeocodeResults(localities)) + .catch(handleError); + } +} + +function handleGeocodeResults( + localities: woosmap.map.localities.LocalitiesGeocodeResponse, +) { + const location = localities.results[0]?.geometry?.location; + location && handleSearchResults(location); +} + +function handleSearchResults(originalLatLng: woosmap.map.LatLngLiteral) { + if (activeLocation) { + activeLocation.setMap(null); + } + + if (originalLatLng) { + activeLocation = new woosmap.map.Marker({ + position: originalLatLng, + icon: { + url: "https://images.woosmap.com/marker.png", + scaledSize: { + height: 50, + width: 32, + }, + }, + }); + activeLocation.setMap(map); + map.setCenter(originalLatLng); + map.setZoom(8); + } +} + +function selectStoreOnList(storeId: string) { + const storeList = document.querySelector(".stores-list"); + if (storeList) { + const storeElement = Array.from(storeList.children).find( + (child) => child.getAttribute("data-store-id") == storeId, + ); + if (storeElement) { + Array.from(storeList.children).forEach((child) => + child.classList.remove("active"), + ); + storeElement.scrollIntoView(); + storeElement.classList.add("active"); + } + } +} +function initMap() { + map = new window.woosmap.map.Map( + document.getElementById("map") as HTMLElement, + mapOptions, + ); + storesOverlay = new woosmap.map.StoresOverlay(storesStyle); + storesOverlay.setMap(map); + map.addListener("bounds_changed", () => { + const bounds = map.getBounds(); + visibleStores = filterStoresByBounds(allStores, bounds); + displayListStores(visibleStores); + }); + window.woosmap.map.event.addListener( + map, + "store_selected", + (store: woosmap.map.stores.StoreResponse) => { + selectStoreOnList(store.properties.store_id); + }, + ); + + storesService = new woosmap.map.StoresService(); + localitiesService = new woosmap.map.LocalitiesService(); + + getAllStores().then((stores: woosmap.map.stores.StoreResponse[]) => { + allStores = stores; + displayListStores(stores); + }); +} + +function handleError(error: woosmap.map.errors.APIError) { + console.error("Error:", error); +} + +declare global { + interface Window { + initMap: () => void; + } +} +window.initMap = initMap; +// [END woosmap_stores_list_sync_map] + +export {}; diff --git a/samples/stores-list-sync-map/stores-list-sync-map.json b/samples/stores-list-sync-map/stores-list-sync-map.json new file mode 100644 index 00000000..ef2a0b15 --- /dev/null +++ b/samples/stores-list-sync-map/stores-list-sync-map.json @@ -0,0 +1,15 @@ +{ + "title": "Stores List Sync Map", + "description": "Get all stores recursively. Synchronise store list with map display using map bounds and contains method", + "category": "Services", + "callback": "initMap", + "WOOSMAP_PUBLIC_API_KEY": "woos-5b16337d-8364-303a-854d-86f87b480aa5", + "tag": "stores_list_sync_map", + "name": "stores-list-sync-map", + "pagination": { + "data": "mode", + "size": 1, + "alias": "mode" + }, + "permalink": "samples/{{ page.fileSlug }}/{{mode}}/{% if mode == 'jsfiddle' %}demo{% else %}index{% endif %}.{{ page.outputFileExtension }}" +} diff --git a/samples/stores-list-sync-map/style.scss b/samples/stores-list-sync-map/style.scss new file mode 100644 index 00000000..089cd27d --- /dev/null +++ b/samples/stores-list-sync-map/style.scss @@ -0,0 +1,129 @@ +@use 'sass:meta'; // To enable @use via meta.load-css and keep comments in order + +/* [START woosmap_stores_list_sync_map] */ +@include meta.load-css("../../shared/scss/_sidebar.scss"); + +@import "../../shared/scss/mixins.scss"; + +.btn { + @include button($background: #3d5afe, $display: inline, $position: relative, $height: 40px); +} + +.btnText { + @include actionText($color: #fff); +} + +#sidebar { + flex-basis: 25rem; + box-shadow: 0 -2px 4px 0 rgba(0, 0, 0, 0.12); + display: flex; + flex-direction: column; + overflow: hidden; + z-index: 1; +} + +#stores-panel { + overflow: hidden; + background: #fff; + overflow-y: auto; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;; + scroll-behavior: smooth; + + h3 { + padding: 2rem 1.3rem 1.3rem; + margin-bottom: 0; + outline: 0; + } + + ul { + margin: 0 !important; + padding: 0; + list-style: none; + } + + button { + border: none; + } +} + +#search-container { + padding: 1rem 0.5rem; + background: rgba(250, 251, 252, 1); + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + + #search-input-container { + display: flex; + justify-content: space-between; + } + + h3 { + margin-top: 0; + } +} + +input { + padding: 0.6em; + border: 1px solid #ccc; + box-shadow: inset 0 1px 3px #ddd; + border-radius: 4px; + vertical-align: middle; + font-weight: normal; + letter-spacing: 0.01em; + font-size: 1em; + display: inline-block; + box-sizing: border-box; + width: 100%; + margin-right: 5px; + + &[type="text"]:focus { + outline: 0; + border-color: #03a9f4; + } +} + +.summary-store-card { + display: flex; + padding: 0.8rem 0.8rem; + vertical-align: middle; + border-bottom: 1px solid rgba(0, 0, 0, 0.08); + justify-content: space-between; + cursor: pointer; + + &:hover { + background-color: rgba(0, 0, 0, 0.04); + } + + &.active { + background-color: rgba(0, 0, 0, 0.08); + } +} + +.store-address { + line-height: 1.6rem; +} + +.store-contact a, +.store-website a { + padding: 0.2em 0; + margin-left: 20px; + color: #1d1d1d; + + &::before { + content: ""; + background-size: 16px 16px; + background-repeat: no-repeat; + padding-right: 20px; + vertical-align: middle; + margin-left: -20px; + } +} + +.store-contact a::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 0 24 24' width='24'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath d='M6.54 5c.06.89.21 1.76.45 2.59l-1.2 1.2c-.41-1.2-.67-2.47-.76-3.79h1.51m9.86 12.02c.85.24 1.72.39 2.6.45v1.49c-1.32-.09-2.59-.35-3.8-.75l1.2-1.19M7.5 3H4c-.55 0-1 .45-1 1 0 9.39 7.61 17 17 17 .55 0 1-.45 1-1v-3.49c0-.55-.45-1-1-1-1.24 0-2.45-.2-3.57-.57-.1-.04-.21-.05-.31-.05-.26 0-.51.1-.71.29l-2.2 2.2c-2.83-1.45-5.15-3.76-6.59-6.59l2.2-2.2c.28-.28.36-.67.25-1.02C8.7 6.45 8.5 5.25 8.5 4c0-.55-.45-1-1-1z'/%3E%3C/svg%3E"); +} + +.store-website a::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 0 24 24' width='24'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath d='M17 7h-4v2h4c1.65 0 3 1.35 3 3s-1.35 3-3 3h-4v2h4c2.76 0 5-2.24 5-5s-2.24-5-5-5zm-6 8H7c-1.65 0-3-1.35-3-3s1.35-3 3-3h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-2zm-3-4h8v2H8z'/%3E%3C/svg%3E"); +} + +/* [END woosmap_stores_list_sync_map] */