From e01cc5a4fca6977cac5502f85502081b5551222f Mon Sep 17 00:00:00 2001 From: awasthiruchi Date: Sat, 31 Jan 2026 17:50:36 -0500 Subject: [PATCH 01/18] Refactor locator.js with distance updates and improvements Added some rules like AEM wtb --- widgets/locator/locator.js | 770 ++++++++++++++++++++++++++----------- 1 file changed, 542 insertions(+), 228 deletions(-) diff --git a/widgets/locator/locator.js b/widgets/locator/locator.js index 12e6ce07..66811a97 100644 --- a/widgets/locator/locator.js +++ b/widgets/locator/locator.js @@ -1,6 +1,8 @@ import { loadCSS } from '../../scripts/aem.js'; -const MAX_DISTANCE = 100; +const MAX_DISTANCE = 200; +const EVENTS_MAX_DISTANCE = 50; + const hhRetailersResults = document.querySelector('#locator-hh-retailers-tabpanel'); const hhDistributorsResults = document.querySelector('#locator-hh-distributors-tabpanel'); @@ -51,96 +53,273 @@ const haversineDistance = (lat1, lon1, lat2, lon2) => { }; async function geoCode(address) { - const resp = await fetch(`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=AIzaSyCPlws-m9FD9W0nP-WRR-5ldW2a4nh-t4E`); + const resp = await fetch(`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=AIzaSyCPlws-m9FD9W0nP-WRR-5ldW2a4nh-t4E`); const json = await resp.json(); const { results } = json; - const r0 = results?.[0]; - const comps = r0?.address_components || []; - const findType = (t) => comps.find((c) => Array.isArray(c.types) && c.types.includes(t)); - const countryComp = findType('country'); - const admin1Comp = findType('administrative_area_level_1'); + const countryComponent = results[0]?.address_components?.find((c) => c.types?.includes('country')); + return { - location: r0?.geometry?.location || null, - country: countryComp ? { - short: countryComp.short_name, - long: countryComp.long_name, - type: 'country', - } : null, - region: admin1Comp ? { - short: admin1Comp.short_name, - long: admin1Comp.long_name, - type: 'administrative_area_level_1', - } : null, + location: results[0]?.geometry?.location, + countryShort: countryComponent?.short_name, + countryLong: countryComponent?.long_name, }; } -function findEventsResults(data, location) { - // Household Events - const filteredHHEvents = data.filter((item) => item.PRODUCT_TYPE === 'HH'); - const hhEvents = filteredHHEvents.sort( - (a, b) => haversineDistance(location.lat, location.lng, a.lat, a.lng) - - haversineDistance(location.lat, location.lng, b.lat, b.lng), - ); - // Commercial Events - const filteredCommEvents = data.filter((item) => item.PRODUCT_TYPE === 'COMM'); - const commEvents = filteredCommEvents.sort( - (a, b) => haversineDistance(location.lat, location.lng, a.lat, a.lng) - - haversineDistance(location.lat, location.lng, b.lat, b.lng), - ); +// Common helpers +function norm(v) { + return (v ?? '').toString().trim(); +} +function normLower(v) { + return norm(v).toLowerCase(); +} +function isEnabled(v) { + return ['true', '1', 'yes', 'y'].includes(normLower(v)); +} +function countryMatches(itemCountry, countryShort, countryLong) { + const c = normLower(itemCountry); + return c && (c === normLower(countryShort) || c === normLower(countryLong)); +} +function recordKey(item) { + return [ + item.TYPE, + item.PRODUCT_TYPE, + item.NAME, + item.ADDRESS_1, + item.CITY, + item.STATE_PROVINCE, + item.POSTAL_CODE, + item.COUNTRY, + ].map(normLower).join('|'); +} + +function applyAemRules(rows, { countryShort, countryLong, productType, allowedTypes }) { + const map = new Map(); + + (rows || []).forEach((r) => { + if (!isEnabled(r.ENABLED)) return; + + if (productType && normLower(r.PRODUCT_TYPE) !== normLower(productType)) return; - return { hhEvents, commEvents }; + if (allowedTypes && !allowedTypes.includes(norm(r.TYPE))) return; + + if (!countryMatches(r.COUNTRY, countryShort, countryLong)) return; + + const action = normLower(r.ACTION); + const key = recordKey(r); + + if (action === 'remove') { + map.delete(key); + return; + } + + if (action === '' || action === 'add' || action === 'update') { + map.set(key, r); + } + }); + + return Array.from(map.values()); } -function findCommResults(data, location, countryShort, regionShort) { - // Distributors - const filteredDistributors = data.filter( - (item) => item.TYPE === 'DEALER/DISTRIBUTOR' && haversineDistance(location.lat, location.lng, item.lat, item.lng) < MAX_DISTANCE, +// EVENTS helpers +function excelSerialToDate(serial) { + const excelEpoch = new Date(1900, 0, 1); + const msPerDay = 24 * 60 * 60 * 1000; + const adjusted = serial > 59 ? serial - 1 : serial; // Excel leap-year bug + return new Date(excelEpoch.getTime() + (adjusted - 1) * msPerDay); +} + +function parseAnyDate(v) { + if (v == null || v === '') return null; + + if (!Number.isNaN(Number(v)) && String(v).trim() !== '') { + return excelSerialToDate(Number(v)); + } + + const d = new Date(v); + return Number.isNaN(d.getTime()) ? null : d; +} + +function ymdKey(d) { + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, '0'); + const dd = String(d.getDate()).padStart(2, '0'); + return `${yyyy}-${mm}-${dd}`; +} + +function formatMDYFromKey(key) { + const [yyyy, mm, dd] = key.split('-'); + return `${mm}/${dd}/${yyyy}`; +} + +function eventKey(e) { + const s = parseAnyDate(e.START_DATE); + const startKey = s ? ymdKey(s) : ''; + return [ + normLower(e.PRODUCT_TYPE), + normLower(e.NAME), + normLower(e.ADDRESS_1), + normLower(e.CITY), + normLower(e.STATE_PROVINCE), + normLower(e.POSTAL_CODE), + normLower(e.COUNTRY), + startKey, + ].join('|'); +} + +function applyAemRulesEvents(rows, { productType }) { + const map = new Map(); + + (rows || []).forEach((r) => { + if (!isEnabled(r.ENABLED)) return; + if (productType && normLower(r.PRODUCT_TYPE) !== normLower(productType)) return; + + const action = normLower(r.ACTION); + const key = eventKey(r); + + if (action === 'remove') { + map.delete(key); + return; + } + if (action === '' || action === 'add' || action === 'update') { + map.set(key, r); + } + }); + + return Array.from(map.values()); +} + +function filterFutureEvents(rows) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + return (rows || []).filter((e) => { + const start = parseAnyDate(e.START_DATE); + const end = parseAnyDate(e.END_DATE); + const compare = (end || start); + if (!compare) return false; + + compare.setHours(0, 0, 0, 0); + return compare >= today; + }); +} + +function groupByStartDate(rows) { + const groups = new Map(); + + (rows || []).forEach((e) => { + const start = parseAnyDate(e.START_DATE); + if (!start) return; + start.setHours(0, 0, 0, 0); + + const key = ymdKey(start); + if (!groups.has(key)) groups.set(key, []); + groups.get(key).push(e); + }); + + return Array.from(groups.entries()).sort((a, b) => a[0].localeCompare(b[0])); +} + + +function findEventsResults(data, location) { + + const hhClean = applyAemRulesEvents(data, { productType: 'HH' }); + const commClean = applyAemRulesEvents(data, { productType: 'COMM' }); + + const hhFuture = filterFutureEvents(hhClean); + const commFuture = filterFutureEvents(commClean); + + const hhNearby = (hhFuture || []).filter((e) => + haversineDistance(location.lat, location.lng, e.lat, e.lng) <= EVENTS_MAX_DISTANCE ); - const distributors = filteredDistributors.sort( - (a, b) => haversineDistance(location.lat, location.lng, a.lat, a.lng) - - haversineDistance(location.lat, location.lng, b.lat, b.lng), + const commNearby = (commFuture || []).filter((e) => + haversineDistance(location.lat, location.lng, e.lat, e.lng) <= EVENTS_MAX_DISTANCE ); - // Local Representatives (country-based) - const wantCountry = (countryShort || '').toUpperCase(); - const wantRegion = (regionShort || '').toUpperCase(); + + const sortEvents = (arr) => { + arr.sort((a, b) => { + const ad = parseAnyDate(a.START_DATE)?.getTime() ?? 0; + const bd = parseAnyDate(b.START_DATE)?.getTime() ?? 0; + if (ad !== bd) return ad - bd; + return haversineDistance(location.lat, location.lng, a.lat, a.lng) + - haversineDistance(location.lat, location.lng, b.lat, b.lng); + }); + }; + sortEvents(hhNearby); + sortEvents(commNearby); - // Local Representatives (country + region match) - const localRep = data.filter((item) => { - if (item.TYPE !== 'LOCAL REP') return false; - const itemCountry = String(item.COUNTRY || '').toUpperCase(); - const itemRegion = String(item.STATE_PROVINCE || item.STATE || item.PROVINCE || '').toUpperCase(); - return itemCountry === wantCountry && itemRegion === wantRegion; + return { + hhGrouped: groupByStartDate(hhNearby), + commGrouped: groupByStartDate(commNearby), + }; +} + +function findCommResults(data, location, countryShort, countryLong) { + const allowedTypes = ['DEALER/DISTRIBUTOR', 'LOCAL REP']; + + const cleaned = applyAemRules(data, { + countryShort, + countryLong, + productType: 'COMM', + allowedTypes, }); + const distributors = cleaned + .filter((i) => i.TYPE === 'DEALER/DISTRIBUTOR' + && haversineDistance(location.lat, location.lng, i.lat, i.lng) <= MAX_DISTANCE) + .sort((a, b) => + haversineDistance(location.lat, location.lng, a.lat, a.lng) + - haversineDistance(location.lat, location.lng, b.lat, b.lng)); + + const localRep = cleaned + .filter((i) => i.TYPE === 'LOCAL REP' + && haversineDistance(location.lat, location.lng, i.lat, i.lng) <= MAX_DISTANCE) + .sort((a, b) => + haversineDistance(location.lat, location.lng, a.lat, a.lng) + - haversineDistance(location.lat, location.lng, b.lat, b.lng)); + return { distributors, localRep }; } -function findHHResults(data, location, country) { - // Retailers - const filteredRetailers = data.filter( - (item) => item.TYPE === 'RETAILERS' && haversineDistance(location.lat, location.lng, item.lat, item.lng) < MAX_DISTANCE, - ); - const retailers = filteredRetailers.sort( - (a, b) => haversineDistance(location.lat, location.lng, a.lat, a.lng) - - haversineDistance(location.lat, location.lng, b.lat, b.lng), - ); - // Distributors - const filteredDistributors = data.filter((item) => item.TYPE === 'DEALER/DISTRIBUTOR'); - const distributors = filteredDistributors.sort( - (a, b) => haversineDistance(location.lat, location.lng, a.lat, a.lng) - - haversineDistance(location.lat, location.lng, b.lat, b.lng), - ); - // Online - const online = data.filter((item) => item.TYPE === 'ONLINE' && item.COUNTRY === country); + +function findHHResults(data, location, countryShort, countryLong) { + const allowedTypes = ['ONLINE', 'RETAILERS', 'DEALER/DISTRIBUTOR']; + + + const cleaned = applyAemRules(data, { + countryShort, + countryLong, + productType: 'HH', + allowedTypes, + }); + + + const retailers = cleaned + .filter((i) => i.TYPE === 'RETAILERS' + && haversineDistance(location.lat, location.lng, i.lat, i.lng) <= MAX_DISTANCE) + .sort((a, b) => + haversineDistance(location.lat, location.lng, a.lat, a.lng) + - haversineDistance(location.lat, location.lng, b.lat, b.lng)); + + const distributors = cleaned + .filter((i) => i.TYPE === 'DEALER/DISTRIBUTOR' + && haversineDistance(location.lat, location.lng, i.lat, i.lng) <= MAX_DISTANCE) + .sort((a, b) => + haversineDistance(location.lat, location.lng, a.lat, a.lng) + - haversineDistance(location.lat, location.lng, b.lat, b.lng)); + + const online = cleaned + .filter((i) => i.TYPE === 'ONLINE') + .sort((a, b) => normLower(a.NAME).localeCompare(normLower(b.NAME))); return { retailers, distributors, online }; } + function displayCommResults(results, location) { const { distributors, localRep } = results; @@ -155,22 +334,40 @@ function displayCommResults(results, location) { distance.classList.add('locator-distance'); li.append(distance); - const address = document.createElement('a'); - const addressQuery = `${result.NAME} ${result.ADDRESS_1}, ${result.CITY}, ${result.STATE_PROVINCE} ${result.POSTAL_CODE}`; - address.href = `https://maps.google.com/?q=${encodeURIComponent(addressQuery)}`; - address.target = '_blank'; - address.rel = 'noopener noreferrer'; - address.textContent = addressQuery; - address.classList.add('locator-address'); - li.append(address); + if (result.ADDRESS_1) { + const addressWrapper = document.createElement('span'); + addressWrapper.classList.add('locator-address'); + + const addressLabel = document.createElement('strong'); + addressLabel.textContent = 'Address: '; + addressWrapper.append(addressLabel); + + const addressLink = document.createElement('a'); + const addressQuery = `${result.ADDRESS_1}, ${result.CITY}, ${result.STATE_PROVINCE} ${result.POSTAL_CODE}`; + addressLink.href = `https://maps.google.com/?q=${encodeURIComponent(addressQuery)}`; + addressLink.target = '_blank'; + addressLink.rel = 'noopener noreferrer'; + addressLink.textContent = addressQuery; + + addressWrapper.append(addressLink); + li.append(addressWrapper); + } // Phone number if (result.PHONE_NUMBER) { const phoneWrapper = document.createElement('span'); phoneWrapper.classList.add('locator-phone'); + + const phoneLabel = document.createElement('strong'); + phoneLabel.textContent = 'Phone: '; + phoneWrapper.append(phoneLabel); + const phoneLink = document.createElement('a'); phoneLink.href = `tel:${result.PHONE_NUMBER}`; phoneLink.textContent = result.PHONE_NUMBER; + phoneLink.target = '_blank'; + phoneLink.rel = 'noopener noreferrer'; + phoneWrapper.append(phoneLink); li.append(phoneWrapper); } @@ -178,18 +375,25 @@ function displayCommResults(results, location) { if (result.WEB_ADDRESS) { const webWrapper = document.createElement('span'); webWrapper.classList.add('locator-web'); - const webLink = document.createElement('a'); + const webLabel = document.createElement('strong'); + webLabel.textContent = 'Website: '; + webWrapper.append(webLabel); + + const webLink = document.createElement('a'); const webAddress = result.WEB_ADDRESS.startsWith('http') ? result.WEB_ADDRESS : `https://${result.WEB_ADDRESS}`; webLink.href = webAddress; webLink.target = '_blank'; + webLink.rel = 'noopener noreferrer'; webLink.textContent = result.WEB_ADDRESS_LINK_TEXT || result.WEB_ADDRESS; + webWrapper.append(webLink); li.append(webWrapper); } + return li; }; @@ -269,198 +473,310 @@ function displayCommResults(results, location) { } function displayEventsResults(results, location) { - const { hhEvents, commEvents } = results; - const formatDate = (excelDate) => { - // Excel dates are the number of days since January 1, 1900 - // JavaScript dates are milliseconds since January 1, 1970 - // Excel has a bug where it treats 1900 as a leap year, so we adjust for that - const excelEpoch = new Date(1900, 0, 1); // January 1, 1900 - const millisecondsPerDay = 24 * 60 * 60 * 1000; - // Adjust for Excel's leap year bug - const adjustedDays = excelDate > 59 ? excelDate - 1 : excelDate; - const date = new Date(excelEpoch.getTime() + (adjustedDays - 1) * millisecondsPerDay); - return date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); - }; + const { hhGrouped, commGrouped } = results; - const createHHEventResult = (result) => { - const li = document.createElement('li'); - const title = document.createElement('h3'); - title.textContent = result.NAME; - li.append(title); + const renderGroupedList = (container, grouped) => { + container.textContent = ''; - const date = document.createElement('span'); - date.textContent = `${formatDate(result.START_DATE)} - ${formatDate(result.END_DATE)}`; - date.classList.add('locator-date'); - li.append(date); + if (!grouped || grouped.length === 0) { + container.innerHTML = '

No events found

'; + return; + } - const distance = document.createElement('span'); - distance.textContent = `${haversineDistance(location.lat, location.lng, result.lat, result.lng).toFixed(1)} miles away`; - distance.classList.add('locator-distance'); - li.append(distance); + grouped.forEach(([dateKey, items]) => { + const heading = document.createElement('h4'); + heading.classList.add('locator-events-dateheading'); + heading.textContent = formatMDYFromKey(dateKey); + container.append(heading); + + const ol = document.createElement('ol'); + ol.classList.add('locator-events-list'); + + items.forEach((e) => { + const li = document.createElement('li'); + li.classList.add('locator-event-card'); + + const title = document.createElement('h3'); + title.textContent = e.NAME; + li.append(title); + + const start = parseAnyDate(e.START_DATE); + const end = parseAnyDate(e.END_DATE); + if (start) { + const dateLine = document.createElement('span'); + dateLine.classList.add('locator-date'); + const startTxt = start.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); + const endTxt = end + ? end.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }) + : ''; + dateLine.textContent = endTxt ? `${startTxt} - ${endTxt}` : startTxt; + li.append(dateLine); + } + + const dist = document.createElement('span'); + dist.classList.add('locator-distance'); + dist.textContent = `${haversineDistance(location.lat, location.lng, e.lat, e.lng).toFixed(1)} miles away`; + li.append(dist); + + if (e.ADDRESS_1) { + const addressWrapper = document.createElement('span'); + addressWrapper.classList.add('locator-address'); + + const addressLabel = document.createElement('strong'); + addressLabel.textContent = 'Address: '; + addressWrapper.append(addressLabel); + + const addressLink = document.createElement('a'); + const addressQuery = `${e.ADDRESS_1}, ${e.CITY}, ${e.STATE_PROVINCE} ${e.POSTAL_CODE}`.replace(/\s+/g, ' ').trim(); + addressLink.href = `https://maps.google.com/?q=${encodeURIComponent(addressQuery)}`; + addressLink.target = '_blank'; + addressLink.rel = 'noopener noreferrer'; + addressLink.textContent = addressQuery; + + addressWrapper.append(addressLink); + li.append(addressWrapper); + } + + ol.append(li); + }); + + container.append(ol); + }); + }; - const address = document.createElement('a'); - const addressQuery = `${result.NAME} ${result.ADDRESS_1}, ${result.CITY}, ${result.STATE_PROVINCE} ${result.POSTAL_CODE}`; - address.href = `https://maps.google.com/?q=${encodeURIComponent(addressQuery)}`; - address.target = '_blank'; - address.rel = 'noopener noreferrer'; - address.textContent = addressQuery; - address.classList.add('locator-address'); - li.append(address); + // render both tab panels + renderGroupedList(eventsHHResults, hhGrouped); + renderGroupedList(eventsCommResults, commGrouped); - return li; - }; + // calendar (optional) + const calendarEl = document.querySelector('#locator-events-calendar'); + if (!calendarEl) return; - const createCommEventResult = (result) => { - const li = document.createElement('li'); - const title = document.createElement('h3'); - title.textContent = result.NAME; - li.append(title); + const eventDates = new Set(); + const addDatesFromGrouped = (grouped) => (grouped || []).forEach(([k]) => eventDates.add(k)); + addDatesFromGrouped(hhGrouped); + addDatesFromGrouped(commGrouped); - const date = document.createElement('span'); - date.textContent = `${formatDate(result.START_DATE)} - ${formatDate(result.END_DATE)}`; - date.classList.add('locator-date'); - li.append(date); + const today = new Date(); + today.setHours(0, 0, 0, 0); - const distance = document.createElement('span'); - distance.textContent = `${haversineDistance(location.lat, location.lng, result.lat, result.lng).toFixed(1)} miles away`; - distance.classList.add('locator-distance'); - li.append(distance); + let viewYear = today.getFullYear(); + let viewMonth = today.getMonth(); + let activeDateKey = null; - const address = document.createElement('a'); - const addressQuery = `${result.NAME} ${result.ADDRESS_1}, ${result.CITY}, ${result.STATE_PROVINCE} ${result.POSTAL_CODE}`; - address.href = `https://maps.google.com/?q=${encodeURIComponent(addressQuery)}`; - address.target = '_blank'; - address.rel = 'noopener noreferrer'; - address.textContent = addressQuery; - address.classList.add('locator-address'); - li.append(address); + const renderCalendar = () => { + calendarEl.textContent = ''; - return li; - }; + const header = document.createElement('div'); + header.classList.add('locator-cal-header'); - if (hhEvents && hhEvents.length > 0) { - const hhEventList = document.createElement('ol'); - hhEvents.forEach((event) => { - hhEventList.appendChild(createHHEventResult(event)); + const prevBtn = document.createElement('button'); + prevBtn.type = 'button'; + prevBtn.textContent = '‹'; + prevBtn.addEventListener('click', () => { + viewMonth -= 1; + if (viewMonth < 0) { viewMonth = 11; viewYear -= 1; } + renderCalendar(); }); - eventsHHResults.textContent = ''; - eventsHHResults.appendChild(hhEventList); - } else { - eventsHHResults.innerHTML = '

No household events found

'; - } - if (commEvents && commEvents.length > 0) { - const commEventList = document.createElement('ol'); - commEvents.forEach((event) => { - commEventList.appendChild(createCommEventResult(event)); + const nextBtn = document.createElement('button'); + nextBtn.type = 'button'; + nextBtn.textContent = '›'; + nextBtn.addEventListener('click', () => { + viewMonth += 1; + if (viewMonth > 11) { viewMonth = 0; viewYear += 1; } + renderCalendar(); }); - eventsCommResults.textContent = ''; - eventsCommResults.appendChild(commEventList); - } else { - eventsCommResults.innerHTML = '

No commercial events found

'; - } + + const monthTitle = document.createElement('div'); + monthTitle.classList.add('locator-cal-title'); + monthTitle.textContent = new Date(viewYear, viewMonth, 1).toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); + + header.append(prevBtn, monthTitle, nextBtn); + calendarEl.append(header); + + const grid = document.createElement('div'); + grid.classList.add('locator-cal-grid'); + + ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].forEach((d) => { + const cell = document.createElement('div'); + cell.classList.add('locator-cal-dow'); + cell.textContent = d; + grid.append(cell); + }); + + const firstDay = new Date(viewYear, viewMonth, 1); + const startWeekday = firstDay.getDay(); + const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate(); + + for (let i = 0; i < startWeekday; i += 1) { + const blank = document.createElement('div'); + blank.classList.add('locator-cal-cell', 'is-blank'); + grid.append(blank); + } + + for (let day = 1; day <= daysInMonth; day += 1) { + const d = new Date(viewYear, viewMonth, day); + d.setHours(0, 0, 0, 0); + const key = ymdKey(d); + + const cell = document.createElement('button'); + cell.type = 'button'; + cell.classList.add('locator-cal-cell'); + cell.textContent = String(day); + + if (eventDates.has(key)) cell.classList.add('has-event'); + if (activeDateKey === key) cell.classList.add('is-active'); + + cell.addEventListener('click', () => { + activeDateKey = (activeDateKey === key) ? null : key; + + if (activeDateKey) { + const hhFiltered = (hhGrouped || []).filter(([k]) => k === activeDateKey); + const commFiltered = (commGrouped || []).filter(([k]) => k === activeDateKey); + renderGroupedList(eventsHHResults, hhFiltered); + renderGroupedList(eventsCommResults, commFiltered); + } else { + renderGroupedList(eventsHHResults, hhGrouped); + renderGroupedList(eventsCommResults, commGrouped); + } + + renderCalendar(); + }); + + grid.append(cell); + } + + calendarEl.append(grid); + }; + + renderCalendar(); } function displayHHResults(results, location) { const { retailers, distributors, online } = results; + const cleanTel = (v) => (v || '').toString().replace(/[^\d+]/g, ''); + + const appendAddress = (li, result) => { + if (!result.ADDRESS_1) return; + + const addressWrapper = document.createElement('span'); + addressWrapper.classList.add('locator-address'); + + const addressLabel = document.createElement('strong'); + addressLabel.textContent = 'Address: '; + addressWrapper.append(addressLabel); + + const addressLink = document.createElement('a'); + const addressQuery = `${result.ADDRESS_1}, ${result.CITY}, ${result.STATE_PROVINCE} ${result.POSTAL_CODE}`.replace(/\s+/g, ' ').trim(); + addressLink.href = `https://maps.google.com/?q=${encodeURIComponent(addressQuery)}`; + addressLink.target = '_blank'; + addressLink.rel = 'noopener noreferrer'; + addressLink.textContent = addressQuery; + + addressWrapper.append(addressLink); + li.append(addressWrapper); + }; + + const appendWebsite = (li, result) => { + if (!result.WEB_ADDRESS) return; + + const webWrapper = document.createElement('span'); + webWrapper.classList.add('locator-web'); + + const webLabel = document.createElement('strong'); + webLabel.textContent = 'Website: '; + webWrapper.append(webLabel); + + const webLink = document.createElement('a'); + const webAddress = result.WEB_ADDRESS.startsWith('http') + ? result.WEB_ADDRESS + : `https://${result.WEB_ADDRESS}`; + + webLink.href = webAddress; + webLink.target = '_blank'; + webLink.rel = 'noopener noreferrer'; + webLink.textContent = result.WEB_ADDRESS_LINK_TEXT || result.WEB_ADDRESS; + + webWrapper.append(webLink); + li.append(webWrapper); + }; + + const appendPhone = (li, result) => { + if (!result.PHONE_NUMBER) return; + + const phoneWrapper = document.createElement('span'); + phoneWrapper.classList.add('locator-phone'); + + const phoneLabel = document.createElement('strong'); + phoneLabel.textContent = 'Phone: '; + phoneWrapper.append(phoneLabel); + + const phoneLink = document.createElement('a'); + phoneLink.href = `tel:${cleanTel(result.PHONE_NUMBER)}`; + phoneLink.textContent = result.PHONE_NUMBER; + phoneLink.target = '_blank'; + phoneLink.rel = 'noopener noreferrer'; + + phoneWrapper.append(phoneLink); + li.append(phoneWrapper); + }; + + const appendDistance = (li, result) => { + if (!location?.lat || !location?.lng || result.lat == null || result.lng == null) return; + + const distance = document.createElement('span'); + distance.textContent = `${haversineDistance(location.lat, location.lng, result.lat, result.lng).toFixed(1)} miles away`; + distance.classList.add('locator-distance'); + li.append(distance); + }; + const createOnlineResult = (result) => { const li = document.createElement('li'); + const title = document.createElement('h3'); title.textContent = result.NAME; li.append(title); - if (result.WEB_ADDRESS) { - const label = document.createElement('span'); - label.textContent = 'Website: '; - label.classList.add('locator-website-label'); - - const website = document.createElement('a'); - website.href = result.WEB_ADDRESS.startsWith('https://') ? result.WEB_ADDRESS : `https://${result.WEB_ADDRESS}`; - website.textContent = new URL(website.href).hostname; - website.target = '_blank'; - website.rel = 'noopener noreferrer'; - website.classList.add('locator-website'); - label.append(website); - li.append(label); - } + + appendAddress(li, result); + appendWebsite(li, result); + appendPhone(li, result); + return li; }; const createDistributorResult = (result) => { const li = document.createElement('li'); + const title = document.createElement('h3'); title.textContent = result.NAME; li.append(title); - const distance = document.createElement('span'); - distance.textContent = `${haversineDistance(location.lat, location.lng, result.lat, result.lng).toFixed(1)} miles away`; - distance.classList.add('locator-distance'); - li.append(distance); - - const address = document.createElement('a'); - const addressQuery = `${result.NAME} ${result.ADDRESS_1}, ${result.CITY}, ${result.STATE_PROVINCE} ${result.POSTAL_CODE}`; - address.href = `https://maps.google.com/?q=${encodeURIComponent(addressQuery)}`; - address.target = '_blank'; - address.rel = 'noopener noreferrer'; - address.textContent = addressQuery; - address.classList.add('locator-address'); - li.append(address); + appendDistance(li, result); + appendAddress(li, result); + appendWebsite(li, result); + appendPhone(li, result); return li; }; const createRetailerResult = (result) => { const li = document.createElement('li'); + const title = document.createElement('h3'); title.textContent = result.NAME; li.append(title); - const distance = document.createElement('span'); - distance.textContent = `${haversineDistance(location.lat, location.lng, result.lat, result.lng).toFixed(1)} miles away`; - distance.classList.add('locator-distance'); - li.append(distance); - - const address = document.createElement('a'); - const addressQuery = `${result.NAME} ${result.ADDRESS_1}, ${result.CITY}, ${result.STATE_PROVINCE} ${result.POSTAL_CODE}`; - address.href = `https://maps.google.com/?q=${encodeURIComponent(addressQuery)}`; - address.target = '_blank'; - address.rel = 'noopener noreferrer'; - address.textContent = addressQuery; - address.classList.add('locator-address'); - li.append(address); - - if (result.WEB_ADDRESS) { - const label = document.createElement('span'); - label.textContent = 'Website: '; - label.classList.add('locator-website-label'); - - const website = document.createElement('a'); - website.href = result.WEB_ADDRESS.startsWith('https://') ? result.WEB_ADDRESS : `https://${result.WEB_ADDRESS}`; - website.textContent = new URL(website.href).hostname; - website.target = '_blank'; - website.rel = 'noopener noreferrer'; - website.classList.add('locator-website'); - label.append(website); - li.append(label); - } + appendDistance(li, result); + appendAddress(li, result); + appendWebsite(li, result); + appendPhone(li, result); - if (result.PHONE_NUMBER) { - const label = document.createElement('span'); - label.textContent = 'Phone: '; - label.classList.add('locator-phone-label'); - - const phone = document.createElement('a'); - phone.textContent = result.PHONE_NUMBER; - phone.href = `tel:${result.PHONE_NUMBER}`; - phone.target = '_blank'; - phone.rel = 'noopener noreferrer'; - phone.classList.add('locator-phone'); - label.append(phone); - li.append(label); - } return li; }; + // Retailers if (retailers && retailers.length > 0) { const retailerList = document.createElement('ol'); retailers.forEach((retailer) => { @@ -472,6 +788,7 @@ function displayHHResults(results, location) { hhRetailersResults.innerHTML = '

No retailers found

'; } + // Distributors if (distributors && distributors.length > 0) { const distributorList = document.createElement('ol'); distributors.forEach((distributor) => { @@ -483,6 +800,7 @@ function displayHHResults(results, location) { hhDistributorsResults.innerHTML = '

No distributors found

'; } + // Online if (online && online.length > 0) { const onlineList = document.createElement('ol'); online.forEach((item) => { @@ -495,6 +813,7 @@ function displayHHResults(results, location) { } } + export default function decorate(widget) { widget.style.visibility = 'hidden'; loadCSS('/blocks/form/form.css').then(() => widget.removeAttribute('style')); @@ -533,11 +852,11 @@ export default function decorate(widget) { e.preventDefault(); const formData = new FormData(form); const data = Object.fromEntries(formData); - const { location, country, region } = await geoCode(data.address); + const { location, countryShort, countryLong } = await geoCode(data.address); if (data.productType === 'HH') { if (location) { - const results = findHHResults(window.locatorData.HH, location, country?.short); + const results = findHHResults(window.locatorData.HH, location, countryShort, countryLong); displayHHResults(results, location); } else { displayHHResults({}); @@ -547,12 +866,7 @@ export default function decorate(widget) { if (data.productType === 'COMM') { if (location) { - const results = findCommResults( - window.locatorData.COMM, - location, - country?.short, - region?.short, - ); + const results = findCommResults(window.locatorData.COMM, location, countryShort, countryLong); displayCommResults(results, location); } else { displayCommResults({}); @@ -565,7 +879,7 @@ export default function decorate(widget) { const results = findEventsResults(window.locatorData.EVENTS, location); displayEventsResults(results, location); } else { - displayEventsResults({}); + displayEventsResults({ hhGrouped: [], commGrouped: [] }, location); } showType('events'); } From 9199c2ac63d6671f5972e3d4e41c385a4fb2dbc9 Mon Sep 17 00:00:00 2001 From: awasthiruchi Date: Sat, 31 Jan 2026 17:51:42 -0500 Subject: [PATCH 02/18] Added calendar --- widgets/locator/locator.html | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/widgets/locator/locator.html b/widgets/locator/locator.html index 59c262b9..7b8230d9 100644 --- a/widgets/locator/locator.html +++ b/widgets/locator/locator.html @@ -38,10 +38,12 @@

Find Locally

-
-
- - +
+
+
+ + +
@@ -51,9 +53,11 @@

Find Locally

-
-
- +
+
+
+ +
@@ -63,11 +67,17 @@

Find Locally

-
-
- + +
+
+
+ +
+ +
+ From db8ce9099948adcf37c2615fb88bd69b628ebb32 Mon Sep 17 00:00:00 2001 From: awasthiruchi Date: Sat, 31 Jan 2026 17:52:12 -0500 Subject: [PATCH 03/18] Update gap size and add new layout styles --- widgets/locator/locator.css | 125 +++++++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git a/widgets/locator/locator.css b/widgets/locator/locator.css index 5de5a5ff..a609b2d1 100644 --- a/widgets/locator/locator.css +++ b/widgets/locator/locator.css @@ -123,7 +123,7 @@ main > .locator-container.section { position: relative; display: flex; flex-direction: column; - gap: var(--spacing-60); + gap: var(--spacing-100); margin-bottom: var(--spacing-200); padding: var(--spacing-200); background-color: var(--color-white); @@ -196,3 +196,126 @@ main > .locator-container.section { .locator .locator-results[aria-hidden='true'] { display: none; } + +.locator-events-layout{ + display: flex; + gap: 24px; + align-items: flex-start; + + max-width: 1000px; + margin: 0 auto; + padding: var(--spacing-300); + background: var(--color-white); +} + +.locator-events-layout .locator-tabpanels{ + max-width: none; + margin: 0; + padding: 0; + background: transparent; + flex: 1 1 auto; + min-width: 0; +} + +.locator-events-calendar{ + flex: 0 0 360px; + max-width: 360px; + background: var(--color-white); + padding: var(--spacing-200); + border-radius: 8px; +} + +.locator-cal-header{ + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +.locator-cal-title{ + font-weight: 600; +} + +.locator-cal-grid{ + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + gap: 6px; +} + +.locator-cal-dow{ + font-size: 12px; + opacity: 0.8; + text-align: center; + padding: 6px 0; +} + +.locator-cal-cell{ + height: 40px; + border: 1px solid rgba(0,0,0,.15); + border-radius: 6px; + background: transparent; + cursor: pointer; +} + +.locator-cal-cell.is-blank{ + border: none; + background: transparent; + cursor: default; +} + +.locator-cal-cell.has-event{ + outline: 2px solid rgba(180,0,60,.6); +} + +.locator-cal-cell.is-selected{ + outline: 2px solid rgba(180,0,60,1); +} +.locator-hh-layout, +.locator-comm-layout{ + display: flex; + gap: 24px; + align-items: flex-start; + + max-width: 1000px; + margin: 0 auto; + padding: var(--spacing-300); + background-color: var(--color-white); +} + +/* IMPORTANT: override global .locator-tabpanels styles INSIDE these layouts */ +.locator-hh-layout .locator-tabpanels, +.locator-comm-layout .locator-tabpanels{ + max-width: none; + margin: 0; + padding: 0; + background: transparent; + flex: 1 1 auto; + min-width: 0; +} + + +/* Mobile */ +@media (max-width: 900px){ + .locator-events-layout{ + flex-direction: column; + } + .locator-events-calendar{ + flex: 1 1 auto; + max-width: 100%; + width: 100%; + } + .locator-hh-layout, + .locator-comm-layout{ + flex-direction: column; + } + + .locator-hh-side, + .locator-comm-side{ + max-width: 100%; + width: 100%; + } +} + + + From ea6b959212afdb87ea33370c4fbdc9bc2ab017eb Mon Sep 17 00:00:00 2001 From: awasthiruchi Date: Fri, 6 Feb 2026 12:38:31 -0500 Subject: [PATCH 04/18] Update geocode API endpoint to new service --- widgets/locator/locator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widgets/locator/locator.js b/widgets/locator/locator.js index 66811a97..519b159e 100644 --- a/widgets/locator/locator.js +++ b/widgets/locator/locator.js @@ -53,7 +53,7 @@ const haversineDistance = (lat1, lon1, lat2, lon2) => { }; async function geoCode(address) { - const resp = await fetch(`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=AIzaSyCPlws-m9FD9W0nP-WRR-5ldW2a4nh-t4E`); + const resp = await fetch(`https://helix-geocode.adobeaem.workers.dev/?address=${encodeURIComponent(address)}`); const json = await resp.json(); const { results } = json; From 88f33debb7bcae39bd8a5eb01623f12e50263ea0 Mon Sep 17 00:00:00 2001 From: awasthiruchi Date: Fri, 13 Feb 2026 18:40:44 -0500 Subject: [PATCH 05/18] Increase EVENTS_MAX_DISTANCE and update fetch limit --- widgets/locator/locator.js | 80 ++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/widgets/locator/locator.js b/widgets/locator/locator.js index 519b159e..5c3f537b 100644 --- a/widgets/locator/locator.js +++ b/widgets/locator/locator.js @@ -1,8 +1,7 @@ import { loadCSS } from '../../scripts/aem.js'; const MAX_DISTANCE = 200; -const EVENTS_MAX_DISTANCE = 50; - +const EVENTS_MAX_DISTANCE = 500; const hhRetailersResults = document.querySelector('#locator-hh-retailers-tabpanel'); const hhDistributorsResults = document.querySelector('#locator-hh-distributors-tabpanel'); @@ -32,7 +31,7 @@ async function fetchData(form) { window.locatorData = {}; window.locatorData.HH = await fetchSheet('https://main--thinktanked--davidnuescheler.aem.live/vitamix/storelocations-hh.json?limit=10000'); window.locatorData.COMM = await fetchSheet('https://main--thinktanked--davidnuescheler.aem.live/vitamix/storelocations-comm.json?limit=2000'); - window.locatorData.EVENTS = await fetchSheet('https://main--thinktanked--davidnuescheler.aem.live/vitamix/storelocations-events.json'); + window.locatorData.EVENTS = await fetchSheet('https://main--thinktanked--davidnuescheler.aem.live/vitamix/storelocations-events.json?limit=3000'); form.dataset.status = 'loaded'; return window.locatorData; } @@ -66,7 +65,6 @@ async function geoCode(address) { }; } - // Common helpers function norm(v) { return (v ?? '').toString().trim(); @@ -94,7 +92,9 @@ function recordKey(item) { ].map(normLower).join('|'); } -function applyAemRules(rows, { countryShort, countryLong, productType, allowedTypes }) { +function applyAemRules(rows, { + countryShort, countryLong, productType, allowedTypes, +}) { const map = new Map(); (rows || []).forEach((r) => { @@ -221,23 +221,20 @@ function groupByStartDate(rows) { return Array.from(groups.entries()).sort((a, b) => a[0].localeCompare(b[0])); } - function findEventsResults(data, location) { - const hhClean = applyAemRulesEvents(data, { productType: 'HH' }); const commClean = applyAemRulesEvents(data, { productType: 'COMM' }); const hhFuture = filterFutureEvents(hhClean); const commFuture = filterFutureEvents(commClean); - const hhNearby = (hhFuture || []).filter((e) => - haversineDistance(location.lat, location.lng, e.lat, e.lng) <= EVENTS_MAX_DISTANCE + const hhNearby = (hhFuture || []).filter( + (e) => haversineDistance(location.lat, location.lng, e.lat, e.lng) <= EVENTS_MAX_DISTANCE, ); - const commNearby = (commFuture || []).filter((e) => - haversineDistance(location.lat, location.lng, e.lat, e.lng) <= EVENTS_MAX_DISTANCE + const commNearby = (commFuture || []).filter( + (e) => haversineDistance(location.lat, location.lng, e.lat, e.lng) <= EVENTS_MAX_DISTANCE, ); - const sortEvents = (arr) => { arr.sort((a, b) => { const ad = parseAnyDate(a.START_DATE)?.getTime() ?? 0; @@ -269,27 +266,21 @@ function findCommResults(data, location, countryShort, countryLong) { const distributors = cleaned .filter((i) => i.TYPE === 'DEALER/DISTRIBUTOR' && haversineDistance(location.lat, location.lng, i.lat, i.lng) <= MAX_DISTANCE) - .sort((a, b) => - haversineDistance(location.lat, location.lng, a.lat, a.lng) + .sort((a, b) => haversineDistance(location.lat, location.lng, a.lat, a.lng) - haversineDistance(location.lat, location.lng, b.lat, b.lng)); const localRep = cleaned .filter((i) => i.TYPE === 'LOCAL REP' && haversineDistance(location.lat, location.lng, i.lat, i.lng) <= MAX_DISTANCE) - .sort((a, b) => - haversineDistance(location.lat, location.lng, a.lat, a.lng) + .sort((a, b) => haversineDistance(location.lat, location.lng, a.lat, a.lng) - haversineDistance(location.lat, location.lng, b.lat, b.lng)); return { distributors, localRep }; } - - - function findHHResults(data, location, countryShort, countryLong) { const allowedTypes = ['ONLINE', 'RETAILERS', 'DEALER/DISTRIBUTOR']; - const cleaned = applyAemRules(data, { countryShort, countryLong, @@ -297,19 +288,16 @@ function findHHResults(data, location, countryShort, countryLong) { allowedTypes, }); - const retailers = cleaned .filter((i) => i.TYPE === 'RETAILERS' && haversineDistance(location.lat, location.lng, i.lat, i.lng) <= MAX_DISTANCE) - .sort((a, b) => - haversineDistance(location.lat, location.lng, a.lat, a.lng) + .sort((a, b) => haversineDistance(location.lat, location.lng, a.lat, a.lng) - haversineDistance(location.lat, location.lng, b.lat, b.lng)); const distributors = cleaned .filter((i) => i.TYPE === 'DEALER/DISTRIBUTOR' && haversineDistance(location.lat, location.lng, i.lat, i.lng) <= MAX_DISTANCE) - .sort((a, b) => - haversineDistance(location.lat, location.lng, a.lat, a.lng) + .sort((a, b) => haversineDistance(location.lat, location.lng, a.lat, a.lng) - haversineDistance(location.lat, location.lng, b.lat, b.lng)); const online = cleaned @@ -319,7 +307,6 @@ function findHHResults(data, location, countryShort, countryLong) { return { retailers, distributors, online }; } - function displayCommResults(results, location) { const { distributors, localRep } = results; @@ -615,6 +602,23 @@ function displayEventsResults(results, location) { grid.append(blank); } + const createCellClickHandler = (cellKey) => () => { + activeDateKey = activeDateKey === cellKey ? null : cellKey; + + if (activeDateKey) { + const hhFiltered = (hhGrouped || []).filter(([k]) => k === activeDateKey); + const commFiltered = (commGrouped || []).filter(([k]) => k === activeDateKey); + + renderGroupedList(eventsHHResults, hhFiltered); + renderGroupedList(eventsCommResults, commFiltered); + } else { + renderGroupedList(eventsHHResults, hhGrouped); + renderGroupedList(eventsCommResults, commGrouped); + } + + renderCalendar(); + }; + for (let day = 1; day <= daysInMonth; day += 1) { const d = new Date(viewYear, viewMonth, day); d.setHours(0, 0, 0, 0); @@ -628,21 +632,7 @@ function displayEventsResults(results, location) { if (eventDates.has(key)) cell.classList.add('has-event'); if (activeDateKey === key) cell.classList.add('is-active'); - cell.addEventListener('click', () => { - activeDateKey = (activeDateKey === key) ? null : key; - - if (activeDateKey) { - const hhFiltered = (hhGrouped || []).filter(([k]) => k === activeDateKey); - const commFiltered = (commGrouped || []).filter(([k]) => k === activeDateKey); - renderGroupedList(eventsHHResults, hhFiltered); - renderGroupedList(eventsCommResults, commFiltered); - } else { - renderGroupedList(eventsHHResults, hhGrouped); - renderGroupedList(eventsCommResults, commGrouped); - } - - renderCalendar(); - }); + cell.addEventListener('click', createCellClickHandler(key)); grid.append(cell); } @@ -813,7 +803,6 @@ function displayHHResults(results, location) { } } - export default function decorate(widget) { widget.style.visibility = 'hidden'; loadCSS('/blocks/form/form.css').then(() => widget.removeAttribute('style')); @@ -866,7 +855,12 @@ export default function decorate(widget) { if (data.productType === 'COMM') { if (location) { - const results = findCommResults(window.locatorData.COMM, location, countryShort, countryLong); + const results = findCommResults( + window.locatorData.COMM, + location, + countryShort, + countryLong, + ); displayCommResults(results, location); } else { displayCommResults({}); From 7e2bcfa9588e6754a5c95e047046fd68f8abf77b Mon Sep 17 00:00:00 2001 From: awasthiruchi Date: Mon, 16 Feb 2026 11:28:30 -0500 Subject: [PATCH 06/18] Increase max distance for distributors and local reps --- widgets/locator/locator.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/widgets/locator/locator.js b/widgets/locator/locator.js index 5c3f537b..cb4300d5 100644 --- a/widgets/locator/locator.js +++ b/widgets/locator/locator.js @@ -2,6 +2,7 @@ import { loadCSS } from '../../scripts/aem.js'; const MAX_DISTANCE = 200; const EVENTS_MAX_DISTANCE = 500; +const MAX_DISTANCE_COMM = 1000; const hhRetailersResults = document.querySelector('#locator-hh-retailers-tabpanel'); const hhDistributorsResults = document.querySelector('#locator-hh-distributors-tabpanel'); @@ -265,13 +266,13 @@ function findCommResults(data, location, countryShort, countryLong) { const distributors = cleaned .filter((i) => i.TYPE === 'DEALER/DISTRIBUTOR' - && haversineDistance(location.lat, location.lng, i.lat, i.lng) <= MAX_DISTANCE) + && haversineDistance(location.lat, location.lng, i.lat, i.lng) <= MAX_DISTANCE_COMM) .sort((a, b) => haversineDistance(location.lat, location.lng, a.lat, a.lng) - haversineDistance(location.lat, location.lng, b.lat, b.lng)); const localRep = cleaned .filter((i) => i.TYPE === 'LOCAL REP' - && haversineDistance(location.lat, location.lng, i.lat, i.lng) <= MAX_DISTANCE) + && haversineDistance(location.lat, location.lng, i.lat, i.lng) <= MAX_DISTANCE_COMM) .sort((a, b) => haversineDistance(location.lat, location.lng, a.lat, a.lng) - haversineDistance(location.lat, location.lng, b.lat, b.lng)); From 052209bde76644c7b694966a57a6a5e2edb14f5d Mon Sep 17 00:00:00 2001 From: awasthiruchi Date: Thu, 19 Feb 2026 18:33:16 -0500 Subject: [PATCH 07/18] Refactor geoCode function and update state handling --- widgets/locator/locator.js | 64 +++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/widgets/locator/locator.js b/widgets/locator/locator.js index cb4300d5..46d60a0a 100644 --- a/widgets/locator/locator.js +++ b/widgets/locator/locator.js @@ -1,7 +1,7 @@ import { loadCSS } from '../../scripts/aem.js'; const MAX_DISTANCE = 200; -const EVENTS_MAX_DISTANCE = 500; +const EVENTS_MAX_DISTANCE = 100; const MAX_DISTANCE_COMM = 1000; const hhRetailersResults = document.querySelector('#locator-hh-retailers-tabpanel'); @@ -53,17 +53,43 @@ const haversineDistance = (lat1, lon1, lat2, lon2) => { }; async function geoCode(address) { - const resp = await fetch(`https://helix-geocode.adobeaem.workers.dev/?address=${encodeURIComponent(address)}`); - const json = await resp.json(); - const { results } = json; + try { + const resp = await fetch( + `https://helix-geocode.adobeaem.workers.dev/?address=${encodeURIComponent(address)}`, + ); - const countryComponent = results[0]?.address_components?.find((c) => c.types?.includes('country')); + const json = await resp.json(); + const results = json?.results; - return { - location: results[0]?.geometry?.location, - countryShort: countryComponent?.short_name, - countryLong: countryComponent?.long_name, - }; + if (!results || !results.length) { + return null; + } + + const result = results[0]; + const components = result?.address_components || []; + + const getComponent = (type) => components.find((c) => c.types?.includes(type)); + + const countryComponent = getComponent('country'); + const stateComponent = getComponent('administrative_area_level_1'); + + return { + // Geo Location + location: result?.geometry?.location || null, + + // Country + countryShort: countryComponent?.short_name || null, + countryLong: countryComponent?.long_name || null, + + // State + stateShort: stateComponent?.short_name || null, + stateLong: stateComponent?.long_name || null, + + }; + } catch (error) { + console.error('Geocode error:', error); + return null; + } } // Common helpers @@ -254,12 +280,14 @@ function findEventsResults(data, location) { }; } -function findCommResults(data, location, countryShort, countryLong) { +function findCommResults(data, location, countryShort, countryLong, stateShort, stateLong) { const allowedTypes = ['DEALER/DISTRIBUTOR', 'LOCAL REP']; const cleaned = applyAemRules(data, { countryShort, countryLong, + stateShort, + stateLong, productType: 'COMM', allowedTypes, }); @@ -272,9 +300,7 @@ function findCommResults(data, location, countryShort, countryLong) { const localRep = cleaned .filter((i) => i.TYPE === 'LOCAL REP' - && haversineDistance(location.lat, location.lng, i.lat, i.lng) <= MAX_DISTANCE_COMM) - .sort((a, b) => haversineDistance(location.lat, location.lng, a.lat, a.lng) - - haversineDistance(location.lat, location.lng, b.lat, b.lng)); + && (i.STATE_PROVINCE === stateShort || i.STATE_NAME === stateLong)); return { distributors, localRep }; } @@ -842,7 +868,13 @@ export default function decorate(widget) { e.preventDefault(); const formData = new FormData(form); const data = Object.fromEntries(formData); - const { location, countryShort, countryLong } = await geoCode(data.address); + const { + location, + countryShort, + countryLong, + stateShort, + stateLong, + } = await geoCode(data.address); if (data.productType === 'HH') { if (location) { @@ -861,6 +893,8 @@ export default function decorate(widget) { location, countryShort, countryLong, + stateShort, + stateLong, ); displayCommResults(results, location); } else { From f97a45d27cec671fa1d1e181654802398f169d6e Mon Sep 17 00:00:00 2001 From: awasthiruchi Date: Thu, 19 Feb 2026 18:36:01 -0500 Subject: [PATCH 08/18] Refactor geoCode function for improved readability --- widgets/locator/locator.js | 51 +++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/widgets/locator/locator.js b/widgets/locator/locator.js index 46d60a0a..42812fce 100644 --- a/widgets/locator/locator.js +++ b/widgets/locator/locator.js @@ -53,43 +53,38 @@ const haversineDistance = (lat1, lon1, lat2, lon2) => { }; async function geoCode(address) { - try { - const resp = await fetch( - `https://helix-geocode.adobeaem.workers.dev/?address=${encodeURIComponent(address)}`, - ); + const resp = await fetch( + `https://helix-geocode.adobeaem.workers.dev/?address=${encodeURIComponent(address)}`, + ); - const json = await resp.json(); - const results = json?.results; + const json = await resp.json(); + const results = json?.results; - if (!results || !results.length) { - return null; - } + if (!results || !results.length) { + return null; + } - const result = results[0]; - const components = result?.address_components || []; + const result = results[0]; + const components = result?.address_components || []; - const getComponent = (type) => components.find((c) => c.types?.includes(type)); + const getComponent = (type) => components.find((c) => c.types?.includes(type)); - const countryComponent = getComponent('country'); - const stateComponent = getComponent('administrative_area_level_1'); + const countryComponent = getComponent('country'); + const stateComponent = getComponent('administrative_area_level_1'); - return { - // Geo Location - location: result?.geometry?.location || null, + return { + // Geo Location + location: result?.geometry?.location || null, - // Country - countryShort: countryComponent?.short_name || null, - countryLong: countryComponent?.long_name || null, + // Country + countryShort: countryComponent?.short_name || null, + countryLong: countryComponent?.long_name || null, - // State - stateShort: stateComponent?.short_name || null, - stateLong: stateComponent?.long_name || null, + // State + stateShort: stateComponent?.short_name || null, + stateLong: stateComponent?.long_name || null, - }; - } catch (error) { - console.error('Geocode error:', error); - return null; - } + }; } // Common helpers From 41161f7a952ce5e2d311ccba3729a0707df8611f Mon Sep 17 00:00:00 2001 From: awasthiruchi Date: Mon, 2 Mar 2026 15:25:27 -0500 Subject: [PATCH 09/18] Remove distance element from locator display Removed distance display from locator results. --- widgets/locator/locator.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/widgets/locator/locator.js b/widgets/locator/locator.js index 42812fce..58ef7abd 100644 --- a/widgets/locator/locator.js +++ b/widgets/locator/locator.js @@ -338,11 +338,6 @@ function displayCommResults(results, location) { title.textContent = result.NAME; li.append(title); - const distance = document.createElement('span'); - distance.textContent = `${haversineDistance(location.lat, location.lng, result.lat, result.lng).toFixed(1)} miles away`; - distance.classList.add('locator-distance'); - li.append(distance); - if (result.ADDRESS_1) { const addressWrapper = document.createElement('span'); addressWrapper.classList.add('locator-address'); From 2422439f6f26e14ec0c3e24d3f4ced8c3b7877db Mon Sep 17 00:00:00 2001 From: awasthiruchi Date: Mon, 2 Mar 2026 15:27:18 -0500 Subject: [PATCH 10/18] Update store location URLs in locator.js --- widgets/locator/locator.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/widgets/locator/locator.js b/widgets/locator/locator.js index 58ef7abd..194900f9 100644 --- a/widgets/locator/locator.js +++ b/widgets/locator/locator.js @@ -30,9 +30,9 @@ async function fetchData(form) { form.dataset.status = 'loading'; window.locatorData = {}; - window.locatorData.HH = await fetchSheet('https://main--thinktanked--davidnuescheler.aem.live/vitamix/storelocations-hh.json?limit=10000'); - window.locatorData.COMM = await fetchSheet('https://main--thinktanked--davidnuescheler.aem.live/vitamix/storelocations-comm.json?limit=2000'); - window.locatorData.EVENTS = await fetchSheet('https://main--thinktanked--davidnuescheler.aem.live/vitamix/storelocations-events.json?limit=3000'); + window.locatorData.HH = await fetchSheet('https://main--vitamix--aemsites.aem.live/us/en_us/where-to-buy/storelocations-hh.json'); + window.locatorData.COMM = await fetchSheet('https://main--vitamix--aemsites.aem.live/us/en_us/where-to-buy/storelocations-comm.json'); + window.locatorData.EVENTS = await fetchSheet('https://main--vitamix--aemsites.aem.live/us/en_us/where-to-buy/storelocations-events.json'); form.dataset.status = 'loaded'; return window.locatorData; } From 494f26cc089d218cf4b1a58068cb2e3ac36754dd Mon Sep 17 00:00:00 2001 From: awasthiruchi Date: Mon, 2 Mar 2026 15:35:21 -0500 Subject: [PATCH 11/18] Remove distance information from locator items Removed distance display from locator results. --- widgets/locator/locator.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/widgets/locator/locator.js b/widgets/locator/locator.js index 194900f9..b6fbef71 100644 --- a/widgets/locator/locator.js +++ b/widgets/locator/locator.js @@ -406,12 +406,7 @@ function displayCommResults(results, location) { const title = document.createElement('h3'); title.textContent = result.NAME; li.append(title); - - const distance = document.createElement('span'); - distance.textContent = `${haversineDistance(location.lat, location.lng, result.lat, result.lng).toFixed(1)} miles away`; - distance.classList.add('locator-distance'); - li.append(distance); - + // Phone number if (result.PHONE_NUMBER) { const phoneWrapper = document.createElement('span'); From 3c006cbc1c275b0058f9d203edfbcd5d6c0eb485 Mon Sep 17 00:00:00 2001 From: awasthiruchi Date: Fri, 13 Mar 2026 13:21:10 -0400 Subject: [PATCH 12/18] Add email display functionality to locator --- widgets/locator/locator.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/widgets/locator/locator.js b/widgets/locator/locator.js index 4492b1cd..94936198 100644 --- a/widgets/locator/locator.js +++ b/widgets/locator/locator.js @@ -741,6 +741,26 @@ function displayHHResults(results, location, labels = {}) { li.append(phoneWrapper); }; + const appendEmail = (li, result) => { + if (!result.EMAIL) return; + + const emailWrapper = document.createElement('span'); + emailWrapper.classList.add('locator-email'); + + const emailLabel = document.createElement('strong'); + emailLabel.textContent = 'Email: '; + emailWrapper.append(emailLabel); + + const emailLink = document.createElement('a'); + emailLink.href = `mailto:${result.EMAIL}`; + emailLink.textContent = result.EMAIL; + emailLink.target = '_blank'; + emailLink.rel = 'noopener noreferrer'; + + emailWrapper.append(emailLink); + li.append(emailWrapper); + }; + const appendDistance = (li, result) => { if (!location?.lat || !location?.lng || result.lat == null || result.lng == null) return; @@ -761,6 +781,7 @@ function displayHHResults(results, location, labels = {}) { appendAddress(li, result); appendWebsite(li, result); appendPhone(li, result); + appendEmail(li, result); return li; }; @@ -776,6 +797,7 @@ function displayHHResults(results, location, labels = {}) { appendAddress(li, result); appendWebsite(li, result); appendPhone(li, result); + appendEmail(li, result); return li; }; @@ -791,6 +813,7 @@ function displayHHResults(results, location, labels = {}) { appendAddress(li, result); appendWebsite(li, result); appendPhone(li, result); + appendEmail(li, result); return li; }; From 862036b18aa08e6faff64cabf2357f4f0665dc78 Mon Sep 17 00:00:00 2001 From: awasthiruchi Date: Fri, 27 Mar 2026 15:22:49 -0400 Subject: [PATCH 13/18] Refactor locator.js and update data source URLs Refactor locator.js to improve code clarity and update data fetching URLs. --- widgets/locator/locator.js | 148 +++++++++++++++++-------------------- 1 file changed, 69 insertions(+), 79 deletions(-) diff --git a/widgets/locator/locator.js b/widgets/locator/locator.js index 94936198..e5d1e3b7 100644 --- a/widgets/locator/locator.js +++ b/widgets/locator/locator.js @@ -1,24 +1,9 @@ import { loadCSS } from '../../scripts/aem.js'; -import { getLocaleAndLanguage } from '../../scripts/scripts.js'; - -/** - * Load widget copy from the widget's local JSON (same name as the script). - * @param {string} lang - Language key (e.g. en, fr) - * @returns {Promise} Copy for that language (e.g. { labels: { ... } }) - */ -async function loadWidgetCopy(lang) { - const scriptPath = new URL(import.meta.url).pathname; - const jsonPath = scriptPath.replace(/\.js$/, '.json'); - const url = `${window.hlx?.codeBasePath || ''}${jsonPath}`; - const resp = await fetch(url); - const data = await resp.json(); - const key = data[lang] ? lang : 'en'; - return data[key] || {}; -} const MAX_DISTANCE = 200; const EVENTS_MAX_DISTANCE = 100; -const MAX_DISTANCE_COMM = 1000; +const MAX_DISTANCE_COMM = 50; +const maxDistanceHhDistributors = 1500; const hhRetailersResults = document.querySelector('#locator-hh-retailers-tabpanel'); const hhDistributorsResults = document.querySelector('#locator-hh-distributors-tabpanel'); @@ -46,8 +31,8 @@ async function fetchData(form) { form.dataset.status = 'loading'; window.locatorData = {}; - window.locatorData.HH = await fetchSheet('https://main--vitamix--aemsites.aem.live/us/en_us/where-to-buy/storelocations-hh.json'); - window.locatorData.COMM = await fetchSheet('https://main--vitamix--aemsites.aem.live/us/en_us/where-to-buy/storelocations-comm.json'); + window.locatorData.HH = await fetchSheet('https://main--vitamix--aemsites.aem.network/us/en_us/where-to-buy/storelocations-hh.json'); + window.locatorData.COMM = await fetchSheet('https://main--vitamix--aemsites.aem.live/us/en_us/where-to-buy/storelocations-comm.json?limit=10000'); window.locatorData.EVENTS = await fetchSheet('https://main--vitamix--aemsites.aem.live/us/en_us/where-to-buy/storelocations-events.json'); form.dataset.status = 'loaded'; return window.locatorData; @@ -291,7 +276,14 @@ function findEventsResults(data, location) { }; } -function findCommResults(data, location, countryShort, countryLong, stateShort, stateLong) { +function findCommResults( + data, + location, + countryShort, + countryLong, + stateShort, + stateLong, +) { const allowedTypes = ['DEALER/DISTRIBUTOR', 'LOCAL REP']; const cleaned = applyAemRules(data, { @@ -309,9 +301,25 @@ function findCommResults(data, location, countryShort, countryLong, stateShort, .sort((a, b) => haversineDistance(location.lat, location.lng, a.lat, a.lng) - haversineDistance(location.lat, location.lng, b.lat, b.lng)); + const hasState = !!(stateShort || stateLong); + const localRep = cleaned - .filter((i) => i.TYPE === 'LOCAL REP' - && (i.STATE_PROVINCE === stateShort || i.STATE_NAME === stateLong)); + .filter((i) => i.TYPE === 'LOCAL REP') + .filter((i) => { + // If we have state info, filter by state + if (hasState) { + return ( + norm(i.STATE_PROVINCE) === norm(stateShort) + || norm(i.STATE_NAME) === norm(stateLong) + ); + } + + // If no state info, fallback to country match (or return all LOCAL REP for that country) + return ( + norm(i.COUNTRY_CODE) === norm(countryShort) + || norm(i.COUNTRY_NAME) === norm(countryLong) + ); + }); return { distributors, localRep }; } @@ -334,7 +342,8 @@ function findHHResults(data, location, countryShort, countryLong) { const distributors = cleaned .filter((i) => i.TYPE === 'DEALER/DISTRIBUTOR' - && haversineDistance(location.lat, location.lng, i.lat, i.lng) <= MAX_DISTANCE) + && haversineDistance(location.lat, location.lng, i.lat, i.lng) + <= maxDistanceHhDistributors) .sort((a, b) => haversineDistance(location.lat, location.lng, a.lat, a.lng) - haversineDistance(location.lat, location.lng, b.lat, b.lng)); @@ -345,7 +354,7 @@ function findHHResults(data, location, countryShort, countryLong) { return { retailers, distributors, online }; } -function displayCommResults(results, location, labels = {}) { +function displayCommResults(results, location) { const { distributors, localRep } = results; const createDistributorResult = (result) => { @@ -413,6 +422,24 @@ function displayCommResults(results, location, labels = {}) { webWrapper.append(webLink); li.append(webWrapper); } + // Email + if (result.EMAIL) { + const emailWrapper = document.createElement('span'); + emailWrapper.classList.add('locator-email'); + + const emailLabel = document.createElement('strong'); + emailLabel.textContent = 'Email: '; + emailWrapper.append(emailLabel); + + const emailLink = document.createElement('a'); + emailLink.href = `mailto:${result.EMAIL}`; + emailLink.textContent = result.EMAIL; + emailLink.target = '_blank'; + emailLink.rel = 'noopener noreferrer'; + + emailWrapper.append(emailLink); + li.append(emailWrapper); + } return li; }; @@ -422,13 +449,13 @@ function displayCommResults(results, location, labels = {}) { const title = document.createElement('h3'); title.textContent = result.NAME; li.append(title); - + // Phone number if (result.PHONE_NUMBER) { const phoneWrapper = document.createElement('span'); phoneWrapper.classList.add('locator-phone'); const phoneLabel = document.createElement('strong'); - phoneLabel.textContent = labels.phone ?? 'Phone: '; + phoneLabel.textContent = 'Phone: '; phoneWrapper.append(phoneLabel); const phoneLink = document.createElement('a'); @@ -445,7 +472,7 @@ function displayCommResults(results, location, labels = {}) { webWrapper.classList.add('locator-web'); const webLabel = document.createElement('strong'); - webLabel.textContent = labels.website ?? 'Website: '; + webLabel.textContent = 'Website: '; webWrapper.append(webLabel); const webLink = document.createElement('a'); @@ -472,7 +499,7 @@ function displayCommResults(results, location, labels = {}) { commDistributorsResults.textContent = ''; commDistributorsResults.appendChild(distributorList); } else { - commDistributorsResults.innerHTML = `

${labels.noDistributorsFound ?? 'No distributors found'}

`; + commDistributorsResults.innerHTML = '

No distributors found

'; } if (localRep && localRep.length > 0) { @@ -483,7 +510,7 @@ function displayCommResults(results, location, labels = {}) { commLocalrepResults.textContent = ''; commLocalrepResults.appendChild(localRepList); } else { - commLocalrepResults.innerHTML = `

${labels.noLocalRepFound ?? 'No local representatives found'}

`; + commLocalrepResults.innerHTML = '

No local representatives found

'; } } @@ -671,7 +698,7 @@ function displayEventsResults(results, location) { renderCalendar(); } -function displayHHResults(results, location, labels = {}) { +function displayHHResults(results, location) { const { retailers, distributors, online } = results; const cleanTel = (v) => (v || '').toString().replace(/[^\d+]/g, ''); @@ -765,8 +792,7 @@ function displayHHResults(results, location, labels = {}) { if (!location?.lat || !location?.lng || result.lat == null || result.lng == null) return; const distance = document.createElement('span'); - const milesAway = (labels.milesAway ?? 'miles away'); - distance.textContent = `${haversineDistance(location.lat, location.lng, result.lat, result.lng).toFixed(1)} ${milesAway}`; + distance.textContent = `${haversineDistance(location.lat, location.lng, result.lat, result.lng).toFixed(1)} miles away`; distance.classList.add('locator-distance'); li.append(distance); }; @@ -827,7 +853,7 @@ function displayHHResults(results, location, labels = {}) { hhRetailersResults.textContent = ''; hhRetailersResults.appendChild(retailerList); } else { - hhRetailersResults.innerHTML = `

${labels.noRetailersFound ?? 'No retailers found'}

`; + hhRetailersResults.innerHTML = '

No retailers found

'; } // Distributors @@ -839,7 +865,7 @@ function displayHHResults(results, location, labels = {}) { hhDistributorsResults.textContent = ''; hhDistributorsResults.appendChild(distributorList); } else { - hhDistributorsResults.innerHTML = `

${labels.noDistributorsFound ?? 'No distributors found'}

`; + hhDistributorsResults.innerHTML = '

No distributors found

'; } // Online @@ -851,53 +877,16 @@ function displayHHResults(results, location, labels = {}) { hhOnlineResults.textContent = ''; hhOnlineResults.appendChild(onlineList); } else { - hhOnlineResults.innerHTML = `

${labels.noOnlineFound ?? 'No online retailers found'}

`; + hhOnlineResults.innerHTML = '

No online retailers found

'; } } -export default async function decorate(widget) { +export default function decorate(widget) { widget.style.visibility = 'hidden'; loadCSS('/blocks/form/form.css').then(() => widget.removeAttribute('style')); - const { language } = getLocaleAndLanguage(); - const lang = (language || 'en_us').split('_')[0]; - const copy = await loadWidgetCopy(lang); - const labels = copy.labels || {}; - const form = widget.querySelector('form'); - // Apply copy to static form and headings - const h1 = widget.querySelector('#find-locally'); - if (h1) h1.textContent = labels.findLocally ?? 'Find Locally'; - const locationLabel = widget.querySelector('label[for="location"]'); - if (locationLabel) locationLabel.textContent = labels.yourLocation ?? 'Your Location'; - const addressInput = widget.querySelector('#address'); - if (addressInput) addressInput.placeholder = labels.addressHint ?? 'Address, City, or Zipcode'; - const productTypeLabel = widget.querySelector('label[for="productType"]'); - if (productTypeLabel) productTypeLabel.textContent = labels.whatAreYouLookingFor ?? 'What are you looking for?'; - const productTypeSelect = widget.querySelector('#productType'); - if (productTypeSelect) { - const opts = productTypeSelect.querySelectorAll('option'); - if (opts[0]) opts[0].textContent = labels.householdProducts ?? 'Household Products'; - if (opts[1]) opts[1].textContent = labels.commercialProducts ?? 'Commercial Products'; - if (opts[2]) opts[2].textContent = labels.demonstrations ?? 'Demonstrations'; - } - const submitBtn = widget.querySelector('form button[type="submit"]'); - if (submitBtn) submitBtn.textContent = labels.search ?? 'Search'; - - // Tab labels: HH = Retailers, Online Retailers, Distributors; COMM = Distributors, Local Rep; - // Events = Household Events, Commercial Events - const hhTabs = widget.querySelectorAll('.locator-hh-results .locator-results-tablist button'); - if (hhTabs[0]) hhTabs[0].textContent = labels.retailers ?? 'Retailers'; - if (hhTabs[1]) hhTabs[1].textContent = labels.onlineRetailers ?? 'Online Retailers'; - if (hhTabs[2]) hhTabs[2].textContent = labels.distributors ?? 'Distributors'; - const commTabs = widget.querySelectorAll('.locator-comm-results .locator-results-tablist button'); - if (commTabs[0]) commTabs[0].textContent = labels.distributors ?? 'Distributors'; - if (commTabs[1]) commTabs[1].textContent = labels.localRepresentatives ?? 'Local Representatives'; - const eventsTabs = widget.querySelectorAll('.locator-events-results .locator-results-tablist button'); - if (eventsTabs[0]) eventsTabs[0].textContent = labels.householdEvents ?? 'Household Events'; - if (eventsTabs[1]) eventsTabs[1].textContent = labels.commercialEvents ?? 'Commercial Events'; - // set initial values from query params const queryParams = Object.fromEntries(new URLSearchParams(window.location.search)); Object.entries(queryParams).forEach(([key, value]) => { @@ -908,13 +897,14 @@ export default async function decorate(widget) { // load results data setTimeout(() => fetchData(form), 300); + const tabpanels = widget.querySelectorAll('.locator-tabpanels .locator-tabpanel'); const tablistButtons = widget.querySelectorAll('.locator-results-tablist button'); const showTab = (tabButton) => { tablistButtons.forEach((b) => b.removeAttribute('aria-selected')); - widget.querySelectorAll('.locator-tabpanels .locator-tabpanel').forEach((panel) => panel.setAttribute('aria-hidden', true)); + tabpanels.forEach((panel) => panel.setAttribute('aria-hidden', true)); tabButton.setAttribute('aria-selected', 'true'); const tabpanel = document.getElementById(tabButton.getAttribute('aria-controls')); - if (tabpanel) tabpanel.setAttribute('aria-hidden', false); + tabpanel.setAttribute('aria-hidden', false); }; const showType = (type) => { @@ -942,7 +932,7 @@ export default async function decorate(widget) { const results = findHHResults(window.locatorData.HH, location, countryShort, countryLong); displayHHResults(results, location); } else { - displayHHResults({}, null, labels); + displayHHResults({}); } showType('hh'); } @@ -957,9 +947,9 @@ export default async function decorate(widget) { stateShort, stateLong, ); - displayCommResults(results, location, labels); + displayCommResults(results, location); } else { - displayCommResults({}, null, labels); + displayCommResults({}); } showType('comm'); } @@ -967,7 +957,7 @@ export default async function decorate(widget) { if (data.productType === 'EVENTS') { if (location) { const results = findEventsResults(window.locatorData.EVENTS, location); - displayEventsResults(results, location, labels); + displayEventsResults(results, location); } else { displayEventsResults({ hhGrouped: [], commGrouped: [] }, location); } From 8f8854580d2a41d7d1c7fd690f245b811a8faed8 Mon Sep 17 00:00:00 2001 From: awasthiruchi Date: Fri, 27 Mar 2026 15:26:09 -0400 Subject: [PATCH 14/18] Enhance localization and refactor locator logic Refactor locator.js to improve structure and localization support. --- widgets/locator/locator.js | 148 ++++++++++++++++++++----------------- 1 file changed, 79 insertions(+), 69 deletions(-) diff --git a/widgets/locator/locator.js b/widgets/locator/locator.js index e5d1e3b7..94936198 100644 --- a/widgets/locator/locator.js +++ b/widgets/locator/locator.js @@ -1,9 +1,24 @@ import { loadCSS } from '../../scripts/aem.js'; +import { getLocaleAndLanguage } from '../../scripts/scripts.js'; + +/** + * Load widget copy from the widget's local JSON (same name as the script). + * @param {string} lang - Language key (e.g. en, fr) + * @returns {Promise} Copy for that language (e.g. { labels: { ... } }) + */ +async function loadWidgetCopy(lang) { + const scriptPath = new URL(import.meta.url).pathname; + const jsonPath = scriptPath.replace(/\.js$/, '.json'); + const url = `${window.hlx?.codeBasePath || ''}${jsonPath}`; + const resp = await fetch(url); + const data = await resp.json(); + const key = data[lang] ? lang : 'en'; + return data[key] || {}; +} const MAX_DISTANCE = 200; const EVENTS_MAX_DISTANCE = 100; -const MAX_DISTANCE_COMM = 50; -const maxDistanceHhDistributors = 1500; +const MAX_DISTANCE_COMM = 1000; const hhRetailersResults = document.querySelector('#locator-hh-retailers-tabpanel'); const hhDistributorsResults = document.querySelector('#locator-hh-distributors-tabpanel'); @@ -31,8 +46,8 @@ async function fetchData(form) { form.dataset.status = 'loading'; window.locatorData = {}; - window.locatorData.HH = await fetchSheet('https://main--vitamix--aemsites.aem.network/us/en_us/where-to-buy/storelocations-hh.json'); - window.locatorData.COMM = await fetchSheet('https://main--vitamix--aemsites.aem.live/us/en_us/where-to-buy/storelocations-comm.json?limit=10000'); + window.locatorData.HH = await fetchSheet('https://main--vitamix--aemsites.aem.live/us/en_us/where-to-buy/storelocations-hh.json'); + window.locatorData.COMM = await fetchSheet('https://main--vitamix--aemsites.aem.live/us/en_us/where-to-buy/storelocations-comm.json'); window.locatorData.EVENTS = await fetchSheet('https://main--vitamix--aemsites.aem.live/us/en_us/where-to-buy/storelocations-events.json'); form.dataset.status = 'loaded'; return window.locatorData; @@ -276,14 +291,7 @@ function findEventsResults(data, location) { }; } -function findCommResults( - data, - location, - countryShort, - countryLong, - stateShort, - stateLong, -) { +function findCommResults(data, location, countryShort, countryLong, stateShort, stateLong) { const allowedTypes = ['DEALER/DISTRIBUTOR', 'LOCAL REP']; const cleaned = applyAemRules(data, { @@ -301,25 +309,9 @@ function findCommResults( .sort((a, b) => haversineDistance(location.lat, location.lng, a.lat, a.lng) - haversineDistance(location.lat, location.lng, b.lat, b.lng)); - const hasState = !!(stateShort || stateLong); - const localRep = cleaned - .filter((i) => i.TYPE === 'LOCAL REP') - .filter((i) => { - // If we have state info, filter by state - if (hasState) { - return ( - norm(i.STATE_PROVINCE) === norm(stateShort) - || norm(i.STATE_NAME) === norm(stateLong) - ); - } - - // If no state info, fallback to country match (or return all LOCAL REP for that country) - return ( - norm(i.COUNTRY_CODE) === norm(countryShort) - || norm(i.COUNTRY_NAME) === norm(countryLong) - ); - }); + .filter((i) => i.TYPE === 'LOCAL REP' + && (i.STATE_PROVINCE === stateShort || i.STATE_NAME === stateLong)); return { distributors, localRep }; } @@ -342,8 +334,7 @@ function findHHResults(data, location, countryShort, countryLong) { const distributors = cleaned .filter((i) => i.TYPE === 'DEALER/DISTRIBUTOR' - && haversineDistance(location.lat, location.lng, i.lat, i.lng) - <= maxDistanceHhDistributors) + && haversineDistance(location.lat, location.lng, i.lat, i.lng) <= MAX_DISTANCE) .sort((a, b) => haversineDistance(location.lat, location.lng, a.lat, a.lng) - haversineDistance(location.lat, location.lng, b.lat, b.lng)); @@ -354,7 +345,7 @@ function findHHResults(data, location, countryShort, countryLong) { return { retailers, distributors, online }; } -function displayCommResults(results, location) { +function displayCommResults(results, location, labels = {}) { const { distributors, localRep } = results; const createDistributorResult = (result) => { @@ -422,24 +413,6 @@ function displayCommResults(results, location) { webWrapper.append(webLink); li.append(webWrapper); } - // Email - if (result.EMAIL) { - const emailWrapper = document.createElement('span'); - emailWrapper.classList.add('locator-email'); - - const emailLabel = document.createElement('strong'); - emailLabel.textContent = 'Email: '; - emailWrapper.append(emailLabel); - - const emailLink = document.createElement('a'); - emailLink.href = `mailto:${result.EMAIL}`; - emailLink.textContent = result.EMAIL; - emailLink.target = '_blank'; - emailLink.rel = 'noopener noreferrer'; - - emailWrapper.append(emailLink); - li.append(emailWrapper); - } return li; }; @@ -449,13 +422,13 @@ function displayCommResults(results, location) { const title = document.createElement('h3'); title.textContent = result.NAME; li.append(title); - + // Phone number if (result.PHONE_NUMBER) { const phoneWrapper = document.createElement('span'); phoneWrapper.classList.add('locator-phone'); const phoneLabel = document.createElement('strong'); - phoneLabel.textContent = 'Phone: '; + phoneLabel.textContent = labels.phone ?? 'Phone: '; phoneWrapper.append(phoneLabel); const phoneLink = document.createElement('a'); @@ -472,7 +445,7 @@ function displayCommResults(results, location) { webWrapper.classList.add('locator-web'); const webLabel = document.createElement('strong'); - webLabel.textContent = 'Website: '; + webLabel.textContent = labels.website ?? 'Website: '; webWrapper.append(webLabel); const webLink = document.createElement('a'); @@ -499,7 +472,7 @@ function displayCommResults(results, location) { commDistributorsResults.textContent = ''; commDistributorsResults.appendChild(distributorList); } else { - commDistributorsResults.innerHTML = '

No distributors found

'; + commDistributorsResults.innerHTML = `

${labels.noDistributorsFound ?? 'No distributors found'}

`; } if (localRep && localRep.length > 0) { @@ -510,7 +483,7 @@ function displayCommResults(results, location) { commLocalrepResults.textContent = ''; commLocalrepResults.appendChild(localRepList); } else { - commLocalrepResults.innerHTML = '

No local representatives found

'; + commLocalrepResults.innerHTML = `

${labels.noLocalRepFound ?? 'No local representatives found'}

`; } } @@ -698,7 +671,7 @@ function displayEventsResults(results, location) { renderCalendar(); } -function displayHHResults(results, location) { +function displayHHResults(results, location, labels = {}) { const { retailers, distributors, online } = results; const cleanTel = (v) => (v || '').toString().replace(/[^\d+]/g, ''); @@ -792,7 +765,8 @@ function displayHHResults(results, location) { if (!location?.lat || !location?.lng || result.lat == null || result.lng == null) return; const distance = document.createElement('span'); - distance.textContent = `${haversineDistance(location.lat, location.lng, result.lat, result.lng).toFixed(1)} miles away`; + const milesAway = (labels.milesAway ?? 'miles away'); + distance.textContent = `${haversineDistance(location.lat, location.lng, result.lat, result.lng).toFixed(1)} ${milesAway}`; distance.classList.add('locator-distance'); li.append(distance); }; @@ -853,7 +827,7 @@ function displayHHResults(results, location) { hhRetailersResults.textContent = ''; hhRetailersResults.appendChild(retailerList); } else { - hhRetailersResults.innerHTML = '

No retailers found

'; + hhRetailersResults.innerHTML = `

${labels.noRetailersFound ?? 'No retailers found'}

`; } // Distributors @@ -865,7 +839,7 @@ function displayHHResults(results, location) { hhDistributorsResults.textContent = ''; hhDistributorsResults.appendChild(distributorList); } else { - hhDistributorsResults.innerHTML = '

No distributors found

'; + hhDistributorsResults.innerHTML = `

${labels.noDistributorsFound ?? 'No distributors found'}

`; } // Online @@ -877,16 +851,53 @@ function displayHHResults(results, location) { hhOnlineResults.textContent = ''; hhOnlineResults.appendChild(onlineList); } else { - hhOnlineResults.innerHTML = '

No online retailers found

'; + hhOnlineResults.innerHTML = `

${labels.noOnlineFound ?? 'No online retailers found'}

`; } } -export default function decorate(widget) { +export default async function decorate(widget) { widget.style.visibility = 'hidden'; loadCSS('/blocks/form/form.css').then(() => widget.removeAttribute('style')); + const { language } = getLocaleAndLanguage(); + const lang = (language || 'en_us').split('_')[0]; + const copy = await loadWidgetCopy(lang); + const labels = copy.labels || {}; + const form = widget.querySelector('form'); + // Apply copy to static form and headings + const h1 = widget.querySelector('#find-locally'); + if (h1) h1.textContent = labels.findLocally ?? 'Find Locally'; + const locationLabel = widget.querySelector('label[for="location"]'); + if (locationLabel) locationLabel.textContent = labels.yourLocation ?? 'Your Location'; + const addressInput = widget.querySelector('#address'); + if (addressInput) addressInput.placeholder = labels.addressHint ?? 'Address, City, or Zipcode'; + const productTypeLabel = widget.querySelector('label[for="productType"]'); + if (productTypeLabel) productTypeLabel.textContent = labels.whatAreYouLookingFor ?? 'What are you looking for?'; + const productTypeSelect = widget.querySelector('#productType'); + if (productTypeSelect) { + const opts = productTypeSelect.querySelectorAll('option'); + if (opts[0]) opts[0].textContent = labels.householdProducts ?? 'Household Products'; + if (opts[1]) opts[1].textContent = labels.commercialProducts ?? 'Commercial Products'; + if (opts[2]) opts[2].textContent = labels.demonstrations ?? 'Demonstrations'; + } + const submitBtn = widget.querySelector('form button[type="submit"]'); + if (submitBtn) submitBtn.textContent = labels.search ?? 'Search'; + + // Tab labels: HH = Retailers, Online Retailers, Distributors; COMM = Distributors, Local Rep; + // Events = Household Events, Commercial Events + const hhTabs = widget.querySelectorAll('.locator-hh-results .locator-results-tablist button'); + if (hhTabs[0]) hhTabs[0].textContent = labels.retailers ?? 'Retailers'; + if (hhTabs[1]) hhTabs[1].textContent = labels.onlineRetailers ?? 'Online Retailers'; + if (hhTabs[2]) hhTabs[2].textContent = labels.distributors ?? 'Distributors'; + const commTabs = widget.querySelectorAll('.locator-comm-results .locator-results-tablist button'); + if (commTabs[0]) commTabs[0].textContent = labels.distributors ?? 'Distributors'; + if (commTabs[1]) commTabs[1].textContent = labels.localRepresentatives ?? 'Local Representatives'; + const eventsTabs = widget.querySelectorAll('.locator-events-results .locator-results-tablist button'); + if (eventsTabs[0]) eventsTabs[0].textContent = labels.householdEvents ?? 'Household Events'; + if (eventsTabs[1]) eventsTabs[1].textContent = labels.commercialEvents ?? 'Commercial Events'; + // set initial values from query params const queryParams = Object.fromEntries(new URLSearchParams(window.location.search)); Object.entries(queryParams).forEach(([key, value]) => { @@ -897,14 +908,13 @@ export default function decorate(widget) { // load results data setTimeout(() => fetchData(form), 300); - const tabpanels = widget.querySelectorAll('.locator-tabpanels .locator-tabpanel'); const tablistButtons = widget.querySelectorAll('.locator-results-tablist button'); const showTab = (tabButton) => { tablistButtons.forEach((b) => b.removeAttribute('aria-selected')); - tabpanels.forEach((panel) => panel.setAttribute('aria-hidden', true)); + widget.querySelectorAll('.locator-tabpanels .locator-tabpanel').forEach((panel) => panel.setAttribute('aria-hidden', true)); tabButton.setAttribute('aria-selected', 'true'); const tabpanel = document.getElementById(tabButton.getAttribute('aria-controls')); - tabpanel.setAttribute('aria-hidden', false); + if (tabpanel) tabpanel.setAttribute('aria-hidden', false); }; const showType = (type) => { @@ -932,7 +942,7 @@ export default function decorate(widget) { const results = findHHResults(window.locatorData.HH, location, countryShort, countryLong); displayHHResults(results, location); } else { - displayHHResults({}); + displayHHResults({}, null, labels); } showType('hh'); } @@ -947,9 +957,9 @@ export default function decorate(widget) { stateShort, stateLong, ); - displayCommResults(results, location); + displayCommResults(results, location, labels); } else { - displayCommResults({}); + displayCommResults({}, null, labels); } showType('comm'); } @@ -957,7 +967,7 @@ export default function decorate(widget) { if (data.productType === 'EVENTS') { if (location) { const results = findEventsResults(window.locatorData.EVENTS, location); - displayEventsResults(results, location); + displayEventsResults(results, location, labels); } else { displayEventsResults({ hhGrouped: [], commGrouped: [] }, location); } From 05eee95291d0a6b3efad9465bfaeea61cec12253 Mon Sep 17 00:00:00 2001 From: awasthiruchi Date: Fri, 27 Mar 2026 15:36:17 -0400 Subject: [PATCH 15/18] Fix missing newline at end of locator.js From 933298e2f699e243df28ab9cb9d08273e6187803 Mon Sep 17 00:00:00 2001 From: awasthiruchi Date: Fri, 27 Mar 2026 15:38:02 -0400 Subject: [PATCH 16/18] Increase max distance for HH distributors to 1500 --- widgets/locator/locator.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/widgets/locator/locator.js b/widgets/locator/locator.js index 94936198..3a0665b1 100644 --- a/widgets/locator/locator.js +++ b/widgets/locator/locator.js @@ -19,6 +19,7 @@ async function loadWidgetCopy(lang) { const MAX_DISTANCE = 200; const EVENTS_MAX_DISTANCE = 100; const MAX_DISTANCE_COMM = 1000; +const maxDistanceHhDistributors = 1500; const hhRetailersResults = document.querySelector('#locator-hh-retailers-tabpanel'); const hhDistributorsResults = document.querySelector('#locator-hh-distributors-tabpanel'); @@ -334,7 +335,8 @@ function findHHResults(data, location, countryShort, countryLong) { const distributors = cleaned .filter((i) => i.TYPE === 'DEALER/DISTRIBUTOR' - && haversineDistance(location.lat, location.lng, i.lat, i.lng) <= MAX_DISTANCE) + && haversineDistance(location.lat, location.lng, i.lat, i.lng) + <= maxDistanceHhDistributors) .sort((a, b) => haversineDistance(location.lat, location.lng, a.lat, a.lng) - haversineDistance(location.lat, location.lng, b.lat, b.lng)); From 2c60b084a3e2d737e235f3ea90ba934191980cc4 Mon Sep 17 00:00:00 2001 From: awasthiruchi Date: Mon, 30 Mar 2026 10:48:56 -0400 Subject: [PATCH 17/18] Refactor countryComponent handling in locator.js Refactor countryComponent declaration and add fallback logic. --- widgets/locator/locator.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/widgets/locator/locator.js b/widgets/locator/locator.js index 3a0665b1..d749692e 100644 --- a/widgets/locator/locator.js +++ b/widgets/locator/locator.js @@ -86,9 +86,13 @@ async function geoCode(address) { const getComponent = (type) => components.find((c) => c.types?.includes(type)); - const countryComponent = getComponent('country'); + let countryComponent = getComponent('country'); const stateComponent = getComponent('administrative_area_level_1'); + if (!countryComponent && components.length === 1) { + [countryComponent] = components; + } + return { // Geo Location location: result?.geometry?.location || null, From b9c0bf7490a9f16256ea66632d98feb5406974f6 Mon Sep 17 00:00:00 2001 From: awasthiruchi Date: Mon, 30 Mar 2026 15:32:43 -0400 Subject: [PATCH 18/18] Refactor findCommResults function parameters Refactor findCommResults function to use multiline parameters for better readability. --- widgets/locator/locator.js | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/widgets/locator/locator.js b/widgets/locator/locator.js index d749692e..ac6096df 100644 --- a/widgets/locator/locator.js +++ b/widgets/locator/locator.js @@ -296,7 +296,14 @@ function findEventsResults(data, location) { }; } -function findCommResults(data, location, countryShort, countryLong, stateShort, stateLong) { +function findCommResults( + data, + location, + countryShort, + countryLong, + stateShort, + stateLong, +) { const allowedTypes = ['DEALER/DISTRIBUTOR', 'LOCAL REP']; const cleaned = applyAemRules(data, { @@ -419,7 +426,24 @@ function displayCommResults(results, location, labels = {}) { webWrapper.append(webLink); li.append(webWrapper); } - + // Email + if (result.EMAIL) { + const emailWrapper = document.createElement('span'); + emailWrapper.classList.add('locator-email'); + + const emailLabel = document.createElement('strong'); + emailLabel.textContent = 'Email: '; + emailWrapper.append(emailLabel); + + const emailLink = document.createElement('a'); + emailLink.href = `mailto:${result.EMAIL}`; + emailLink.textContent = result.EMAIL; + emailLink.target = '_blank'; + emailLink.rel = 'noopener noreferrer'; + + emailWrapper.append(emailLink); + li.append(emailWrapper); + } return li; };