PageSourceSearch

https://www.curbsidemexicangrill.com/assets/banner-component-df217…c63f98ee00a1bb6d69b502a08043cecec055cee2.js

js curbsidemexicangrill.com collected 2026-10-08 07:17:54 UTC 15,696 bytes, 411 lines download raw bytes

1const POPUP_SESSION_KEY_PREFIX = 'banner-popup-seen-';
2
3function heapTrack(name) {
4  if (typeof window.heap !== 'undefined' && window.heap.track) {
5    window.heap.track(name);
6  }
7}
8
9function getPopUpPositionStyles(placement) {
10  const px = '24px';
11  switch (placement) {
12    case 'upper_left':
13      return { top: px, left: px };
14    case 'upper_right':
15      return { top: px, right: px };
16    case 'lower_left':
17      return { bottom: px, left: px };
18    case 'lower_right':
19      return { bottom: px, right: px };
20    case 'center':
21    default:
22      return { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' };
23  }
24}
25
26class SiteBanner extends HTMLElement {
27  async connectedCallback() {
28    try {
29      const organizationId = this.getOrganizationId();
30      if (!organizationId) return;
31
32      const banners = await this.fetchBanner(organizationId);
33      if (!banners || banners.length === 0) return;
34
35      const bannerEntry = banners.find((b) => b.announcement_type === 'banner');
36      const popUpEntry = banners.find((b) => b.announcement_type === 'pop_up');
37
38      if (bannerEntry?.message_body) {
39        this.hideDefaultBanner();
40        this.render(bannerEntry);
41      }
42
43      if (popUpEntry?.message_body && !sessionStorage.getItem(POPUP_SESSION_KEY_PREFIX + organizationId)) {
44        this.renderPopUp(popUpEntry, organizationId);
45      }
46    } catch (error) {
47      console.error('Banner: Failed to initialize', error);
48    }
49  }
50
51  getOrganizationId() {
52    return document.body.getAttribute('data-organization-id') ||
53           document.querySelector('meta[name="organization-id"]')?.getAttribute('content') ||
54           window.organizationId ||
55           null;
56  }
57
58  async fetchBanner(organizationId) {
59    const isPreview = window.location.origin === window.siteHost;
60    const url = `${window.chowlyApiHost}/api/v3/public/banners?organization_id=${organizationId}${isPreview ? '&preview=1' : ''}`;
61
62    const response = await fetch(url);
63    if (!response.ok) throw new Error(`API request failed: ${response.status}`);
64
65    const data = await response.json();
66    const banners = Array.isArray(data)
67      ? data
68      : (Array.isArray(data?.banners)
69        ? data.banners
70        : (data && typeof data === 'object' && data.announcement_type ? [data] : []));
71    return banners;
72  }
73
74  hideDefaultBanner() {
75    const defaultBanner = document.querySelector('[data-maglev-section-type="site_notification_01"]');
76    if (defaultBanner) {
77      defaultBanner.style.display = 'none';
78    }
79  }
80  render(banner) {
81    const { message_body, cta_link_url, cta_text, ticker_tape_style, icon } = banner;
82    if (!message_body) return;
83
84    this.className = 'w-full h-[40px] flex justify-center items-center px-4 col-span-2';
85    this.style.backgroundColor = 'var(--brand-primary, #000)';
86    this.setAttribute('role', 'banner');
87    this.setAttribute('aria-label', 'Site notification banner');
88
89    if (ticker_tape_style) {
90      this.style.overflow = 'hidden';
91    }
92
93    const safeIconClass = icon && /^[a-zA-Z0-9\-_\s]+$/.test(icon) ? icon.trim() : '';
94    const messageContent = `${message_body}${cta_link_url ? `
95            <a href="${cta_link_url}"
96               class="text-white text-[14px] font-normal leading-[24px] underline hover:no-underline ml-2"
97               target="_blank"
98               rel="noopener noreferrer">
99              ${cta_text || cta_link_url}
100            </a>
101          ` : ''}`;
102
103    const textClasses = 'text-white text-[14px] font-normal leading-[24px]';
104    const textCenterClasses = ticker_tape_style ? '' : 'text-center line-clamp-2 overflow-hidden';
105    const textStyle = `display: inline-block; ${ticker_tape_style ? 'white-space: nowrap;' : ''}`;
106
107    const iconBlock = safeIconClass
108      ? `<div class="w-[30px] h-[30px] overflow-hidden shrink-0 flex items-center justify-center"><span class="text-white ${safeIconClass}" aria-hidden="true"></span></div>`
109      : '';
110
111    if (ticker_tape_style && safeIconClass) {
112      this.innerHTML = `
113      <section class="flex items-center max-w-[1440px] w-full justify-start">
114        <span class="message ${textClasses} ${textCenterClasses} inline-flex items-center gap-5 flex-nowrap" style="white-space: nowrap;">
115          ${iconBlock}
116          <span>${messageContent}</span>
117        </span>
118      </section>
119    `;
120    } else if (safeIconClass) {
121      this.innerHTML = `
122      <section class="flex items-center gap-[2px] max-w-[1440px] w-full justify-center">
123        <div class="w-[30px] h-[30px] overflow-hidden shrink-0 flex items-center justify-center">
124          <span class="text-white ${safeIconClass}" aria-hidden="true"></span>
125        </div>
126        <span class="message ${textClasses} ${textCenterClasses}" style="${textStyle}">${messageContent}</span>
127      </section>
128    `;
129    } else {
130      this.innerHTML = `
131      <section class="flex items-center space-x-2 max-w-[1440px] w-full ${ticker_tape_style ? 'justify-start' : 'justify-center'}">
132        <span class="message ${textClasses} ${textCenterClasses}" style="${textStyle}">${messageContent}</span>
133      </section>
134    `;
135    }
136
137    if (ticker_tape_style) {
138      this.setupTickerAnimation();
139    }
140  }
141
142  renderPopUp(banner, organizationId) {
143    if (!document.getElementById('banner-popup-styles')) {
144      const style = document.createElement('style');
145      style.id = 'banner-popup-styles';
146      style.textContent = `
147        .banner-popup-overlay {
148          position: fixed;
149          inset: 0;
150          background: var(--dim-overlay, rgba(0,0,0,0.4));
151          z-index: 9999;
152        }
153        .banner-popup-modal {
154          position: absolute;
155          width: 350px;
156          max-width: calc(100vw - 2rem);
157          background: var(--bg-primary, #fff);
158          color: var(--text-primary, #212121);
159          border-radius: 4px;
160          box-shadow: 0 20px 24px -4px rgba(10, 13, 18, 0.10), 0 8px 8px -4px rgba(10, 13, 18, 0.04);
161          overflow: hidden;
162          display: flex;
163          flex-direction: column;
164          max-height: 90vh;
165        }
166        @media (max-width: 640px) {
167          .banner-popup-modal {
168            width: calc(100vw - 2rem) !important;
169            left: 50% !important;
170            top: 50% !important;
171            right: auto !important;
172            bottom: auto !important;
173            transform: translate(-50%, -50%) !important;
174          }
175        }
176        .banner-popup-modal img.banner-popup-image { width: 100%; display: block; max-height: 240px; object-fit: contain; object-position: center; background: var(--bg-secondary, #f2f2f2); }
177        .banner-popup-body { padding: 24px; flex: 1; overflow: auto; }
178        .banner-popup-header { font-size: 18px; font-weight: 600; margin-bottom: 8px; text-align: center; }
179        .banner-popup-message { font-size: 14px; line-height: 24px; text-align: center; }
180        .banner-popup-cta { margin-top: 16px; margin-bottom: 16px; }
181        .banner-popup-cta.banner-popup-cta-standalone { margin-bottom: 0; }
182        .banner-popup-cta .banner-popup-form-button { width: 100%; padding: 10px 16px; background: var(--brand-primary); color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; text-align: center; text-decoration: none; display: block; box-sizing: border-box; }
183        .banner-popup-form { margin-top: 10px; }
184        .banner-popup-form input { width: 100%; padding: 10px 12px; margin-bottom: 12px; border: 1px solid var(--border-secondary); border-radius: 8px; background: var(--bg-form); color: var(--text-primary); font-size: 14px; }
185        .banner-popup-form label { display: block; font-size: 14px; margin-bottom: 4px; color: var(--text-secondary); }
186        .banner-popup-form button, .banner-popup-form .banner-popup-form-button { width: 100%; padding: 10px 16px; background: var(--brand-primary); color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; text-align: center; text-decoration: none; display: block; box-sizing: border-box; }
187        .banner-popup-form .banner-popup-form-button { margin-top: 0; }
188        .banner-popup-close {
189          position: absolute;
190          top: 10px;
191          right: 10px;
192          z-index: 1;
193          padding: 10px;
194          min-width: 44px;
195          min-height: 44px;
196          box-sizing: border-box;
197          border: none;
198          background: transparent;
199          color: var(--text-primary, #212121);
200          cursor: pointer;
201          display: inline-flex;
202          align-items: center;
203          justify-content: center;
204        }
205        .banner-popup-close:hover { opacity: 0.8; }
206        .banner-popup-close-over-image {
207          background: var(--dim-overlay, rgba(0, 0, 0, 0.35));
208          color: #fff;
209          border-radius: 50%;
210        }
211        .banner-popup-close-over-image:hover { opacity: 0.9; }
212        .banner-popup-close-until-image-loads {
213          opacity: 0;
214          pointer-events: none;
215          transition: opacity 0.2s ease;
216        }
217        .banner-popup-close svg { width: 24px; height: 24px; }
218      `;
219      document.head.appendChild(style);
220    }
221
222    const placement = banner.pop_up_placement || 'center';
223    const posStyles = getPopUpPositionStyles(placement);
224
225    const overlay = document.createElement('div');
226    overlay.className = 'banner-popup-overlay';
227    overlay.setAttribute('role', 'dialog');
228    overlay.setAttribute('aria-modal', 'true');
229
230    const modal = document.createElement('div');
231    modal.className = 'banner-popup-modal';
232    Object.assign(modal.style, posStyles);
233
234    const closePopup = () => {
235      document.removeEventListener('keydown', escapeHandler);
236      overlay.remove();
237    };
238    const escapeHandler = (e) => {
239      if (e.key === 'Escape') {
240        heapTrack('popup_dismiss');
241        closePopup();
242      }
243    };
244
245    const closeBtn = document.createElement('button');
246    closeBtn.type = 'button';
247    closeBtn.className = 'banner-popup-close';
248    if (banner.image_url) {
249      closeBtn.classList.add('banner-popup-close-over-image', 'banner-popup-close-until-image-loads');
250    }
251    closeBtn.setAttribute('aria-label', 'Close popup');
252    closeBtn.innerHTML = '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path></svg>';
253    closeBtn.addEventListener('click', (e) => {
254      e.stopPropagation();
255      heapTrack('popup_dismiss');
256      closePopup();
257    });
258    modal.appendChild(closeBtn);
259
260    if (banner.image_url) {
261      const img = document.createElement('img');
262      img.alt = '';
263      img.className = 'banner-popup-image';
264      const showCloseBtn = () => closeBtn.classList.remove('banner-popup-close-until-image-loads');
265      img.onload = showCloseBtn;
266      img.onerror = function () {
267        console.warn('[Banner pop-up] Image failed to load:', banner.image_url, '- Check DevTools → Network for that request (e.g. 403 = forbidden, 404 = not found).');
268        showCloseBtn();
269      };
270      img.src = banner.image_url;
271      if (img.complete) showCloseBtn();
272      modal.appendChild(img);
273    }
274
275    const body = document.createElement('div');
276    body.className = 'banner-popup-body';
277
278    if (banner.header) {
279      const h = document.createElement('div');
280      h.className = 'banner-popup-header';
281      h.textContent = banner.header;
282      body.appendChild(h);
283    }
284
285    const msg = document.createElement('div');
286    msg.className = 'banner-popup-message';
287    msg.textContent = banner.message_body;
288    body.appendChild(msg);
289
290    const hasForm = banner.phone_field || banner.email_field;
291
292    if (banner.cta_link_url && !hasForm) {
293      const cta = document.createElement('div');
294      cta.className = 'banner-popup-cta banner-popup-cta-standalone';
295      const a = document.createElement('a');
296      a.href = banner.cta_link_url;
297      a.target = '_blank';
298      a.rel = 'noopener noreferrer';
299      a.className = 'banner-popup-form-button';
300      a.textContent = banner.cta_text || banner.cta_link_url;
301      a.addEventListener('click', (e) => {
302        heapTrack('popup_cta_click');
303        closePopup();
304      });
305      cta.appendChild(a);
306      body.appendChild(cta);
307    }
308
309    if (hasForm) {
310      const form = document.createElement('form');
311      form.className = 'banner-popup-form';
312      if (banner.email_field) {
313        const label = document.createElement('label');
314        label.textContent = 'Email address';
315        form.appendChild(label);
316        const input = document.createElement('input');
317        input.type = 'email';
318        input.placeholder = 'Email address';
319        input.name = 'email';
320        form.appendChild(input);
321      }
322      if (banner.phone_field) {
323        const label = document.createElement('label');
324        label.textContent = 'Phone number';
325        form.appendChild(label);
326        const input = document.createElement('input');
327        input.type = 'tel';
328        input.placeholder = 'Phone number';
329        input.name = 'phone';
330        form.appendChild(input);
331      }
332      const buttonLabel = banner.cta_text || 'Submit';
333      if (banner.cta_link_url) {
334        const linkBtn = document.createElement('a');
335        linkBtn.href = banner.cta_link_url;
336        linkBtn.target = '_blank';
337        linkBtn.rel = 'noopener noreferrer';
338        linkBtn.className = 'banner-popup-form-button';
339        linkBtn.textContent = buttonLabel;
340        linkBtn.addEventListener('click', () => {
341          heapTrack('popup_cta_click');
342          closePopup();
343        });
344        form.appendChild(linkBtn);
345      } else {
346        const submit = document.createElement('button');
347        submit.type = 'submit';
348        submit.textContent = buttonLabel;
349        form.appendChild(submit);
350      }
351      form.addEventListener('submit', (e) => {
352        e.preventDefault();
353        heapTrack('popup_form_submit');
354        closePopup();
355      });
356      body.appendChild(form);
357    }
358
359    modal.appendChild(body);
360    overlay.appendChild(modal);
361
362    try {
363      sessionStorage.setItem(POPUP_SESSION_KEY_PREFIX + organizationId, '1');
364    } catch (err) {}
365
366    overlay.addEventListener('click', (e) => {
367      if (e.target === overlay) {
368        heapTrack('popup_dismiss');
369        closePopup();
370      }
371    });
372    modal.addEventListener('click', (e) => e.stopPropagation());
373    document.addEventListener('keydown', escapeHandler);
374
375    document.body.appendChild(overlay);
376    heapTrack('popup_open');
377  }
378
379  setupTickerAnimation() {
380    if (!document.getElementById('banner-ticker-animation')) {
381      const style = document.createElement('style');
382      style.id = 'banner-ticker-animation';
383      style.textContent = `
384        @keyframes ticker {
385          from { transform: translateX(0); }
386          to { transform: translateX(-100%); }
387        }
388
389        site-banner .message {
390          padding-left: 100%;
391          animation: ticker 20s linear infinite;
392        }
393      `;
394      document.head.appendChild(style);
395    }
396  }
397}
398
399customElements.define('site-banner', SiteBanner);
400
401if (document.readyState === 'loading') {
402  document.addEventListener('DOMContentLoaded', () => {
403    const skipLink = document.querySelector('a[href="#main-content"]');
404    const insertionPoint = skipLink?.nextElementSibling || document.body.firstChild;
405    insertionPoint?.insertAdjacentHTML('beforebegin', '<site-banner></site-banner>');
406  });
407} else {
408  const skipLink = document.querySelector('a[href="#main-content"]');
409  const insertionPoint = skipLink?.nextElementSibling || document.body.firstChild;
410  insertionPoint?.insertAdjacentHTML('beforebegin', '<site-banner></site-banner>');
411};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.