PageSourceSearch

https://www.littlevenicerestaurant.com/assets/banner-component-2ed…e921f7f4e5b81720e9db242fd824aff0c5ce1686.js

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

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.