diff --git a/widgets/locator/locator.css b/widgets/locator/locator.css index 4853a670..be7d97a7 100644 --- a/widgets/locator/locator.css +++ b/widgets/locator/locator.css @@ -134,7 +134,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); @@ -207,3 +207,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%; + } +} + + + 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 @@
No events found
'; + 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.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'); - 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); + 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 = `${labels.noHouseholdEventsFound ?? '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 = `${labels.noCommercialEventsFound ?? '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); + } + + 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); + 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', createCellClickHandler(key)); + + grid.append(cell); + } + + calendarEl.append(grid); + }; + + renderCalendar(); } function displayHHResults(results, location, labels = {}) { 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 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; + + 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.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 = labels.website ?? '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); + appendEmail(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'); - 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); - - 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); + appendEmail(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'); - 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); - - 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 = labels.website ?? '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); + appendEmail(li, result); - if (result.PHONE_NUMBER) { - const label = document.createElement('span'); - label.textContent = labels.phone ?? '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) => { @@ -494,6 +860,7 @@ function displayHHResults(results, location, labels = {}) { hhRetailersResults.innerHTML = `${labels.noRetailersFound ?? 'No retailers found'}
`; } + // Distributors if (distributors && distributors.length > 0) { const distributorList = document.createElement('ol'); distributors.forEach((distributor) => { @@ -505,6 +872,7 @@ function displayHHResults(results, location, labels = {}) { hhDistributorsResults.innerHTML = `${labels.noDistributorsFound ?? 'No distributors found'}
`; } + // Online if (online && online.length > 0) { const onlineList = document.createElement('ol'); online.forEach((item) => { @@ -591,12 +959,18 @@ export default async 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, + stateShort, + stateLong, + } = await geoCode(data.address); if (data.productType === 'HH') { if (location) { - const results = findHHResults(window.locatorData.HH, location, country?.short); - displayHHResults(results, location, labels); + const results = findHHResults(window.locatorData.HH, location, countryShort, countryLong); + displayHHResults(results, location); } else { displayHHResults({}, null, labels); } @@ -608,8 +982,10 @@ export default async function decorate(widget) { const results = findCommResults( window.locatorData.COMM, location, - country?.short, - region?.short, + countryShort, + countryLong, + stateShort, + stateLong, ); displayCommResults(results, location, labels); } else { @@ -623,7 +999,7 @@ export default async function decorate(widget) { const results = findEventsResults(window.locatorData.EVENTS, location); displayEventsResults(results, location, labels); } else { - displayEventsResults({}, null, labels); + displayEventsResults({ hhGrouped: [], commGrouped: [] }, location); } showType('events'); }