PageSourceSearch

https://www.grtrading.in/assets/js/revamp.js

js grtrading.in collected 2026-10-01 12:12:03 UTC 15,562 bytes, 321 lines download raw bytes

1document.addEventListener('DOMContentLoaded', () => {
2  const toggle = document.querySelector('.nav-toggle');
3  const nav = document.querySelector('.site-nav');
4  const navBackdrop = document.querySelector('.nav-backdrop');
5  const desktopQuery = window.matchMedia('(min-width: 1040px)');
6
7  if (toggle && nav) {
8    // Left slide-in drawer for mobile/tablet nav. Uses a manual focus trap
9    // (Tab/Shift+Tab wrap) plus `inert` while closed, since (unlike the
10    // consultation <dialog>) this is a plain fixed-position panel, not a
11    // native dialog.
12    function openDrawer() {
13      nav.classList.add('is-open');
14      if (navBackdrop) { navBackdrop.hidden = false; requestAnimationFrame(() => navBackdrop.classList.add('is-open')); }
15      document.documentElement.classList.add('nav-open');
16      toggle.setAttribute('aria-expanded', 'true');
17      nav.removeAttribute('inert');
18      nav._returnFocus = document.activeElement;
19      const closeBtn = nav.querySelector('.nav-close');
20      if (closeBtn) closeBtn.focus();
21    }
22
23    function closeDrawer() {
24      nav.classList.remove('is-open');
25      if (navBackdrop) navBackdrop.classList.remove('is-open');
26      document.documentElement.classList.remove('nav-open');
27      toggle.setAttribute('aria-expanded', 'false');
28      if (!desktopQuery.matches) nav.setAttribute('inert', '');
29      if (navBackdrop) setTimeout(() => { if (!nav.classList.contains('is-open')) navBackdrop.hidden = true; }, 300);
30      const returnFocus = nav._returnFocus;
31      if (returnFocus && typeof returnFocus.focus === 'function') returnFocus.focus();
32      else toggle.focus();
33    }
34
35    toggle.addEventListener('click', () => {
36      if (nav.classList.contains('is-open')) closeDrawer(); else openDrawer();
37    });
38
39    document.addEventListener('click', (event) => {
40      if (nav.classList.contains('is-open') && event.target.closest('[data-nav-close]')) closeDrawer();
41    });
42
43    document.addEventListener('keydown', (event) => {
44      if (!nav.classList.contains('is-open')) return;
45      if (event.key === 'Escape') { closeDrawer(); return; }
46      if (event.key !== 'Tab') return;
47      const focusable = nav.querySelectorAll('a[href], button:not([disabled])');
48      if (!focusable.length) return;
49      const first = focusable[0];
50      const last = focusable[focusable.length - 1];
51      if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
52      else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
53    });
54
55    desktopQuery.addEventListener('change', (event) => {
56      if (event.matches) { nav.classList.remove('is-open'); if (navBackdrop) { navBackdrop.classList.remove('is-open'); navBackdrop.hidden = true; } document.documentElement.classList.remove('nav-open'); toggle.setAttribute('aria-expanded', 'false'); nav.removeAttribute('inert'); }
57      else if (!nav.classList.contains('is-open')) { nav.setAttribute('inert', ''); }
58    });
59
60    if (!desktopQuery.matches) nav.setAttribute('inert', '');
61  }
62
63  document.querySelectorAll('.nav-trigger').forEach((trigger) => {
64    trigger.addEventListener('click', () => {
65      if (desktopQuery.matches) return;
66      const group = trigger.closest('.nav-group');
67      if (!group) return;
68      const open = group.classList.toggle('is-expanded');
69      trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
70    });
71  });
72
73  const appRows = document.querySelector('#appRows');
74  const addRow = document.querySelector('#addAppRow');
75  const summary = document.querySelector('#appSummary');
76
77  function rowTemplate() {
78    const div = document.createElement('div');
79    div.className = 'calculator-row';
80    div.innerHTML = '<input name="app_name[]" placeholder="Bundle app"><input name="app_cost[]" type="number" min="0" placeholder="49"><input name="app_feature[]" placeholder="Product bundles"><select name="app_slow[]"><option value="no">Not slow</option><option value="yes">Slowing store</option></select><select name="app_critical[]"><option value="no">Not critical</option><option value="yes">Business-critical</option></select><button class="btn btn-light remove-row" type="button">Remove</button>';
81    return div;
82  }
83
84  function updateCalculator() {
85    if (!appRows || !summary) return;
86    let monthly = 0;
87    let keep = 0;
88    let review = 0;
89    let replace = 0;
90    let savings = 0;
91
92    appRows.querySelectorAll('.calculator-row').forEach((row) => {
93      const cost = Number(row.querySelector('[name="app_cost[]"]').value || 0
93);
94      const slow = row.querySelector('[name="app_slow[]"]').value === 'yes';
95      const critical = row.querySelector('[name="app_critical[]"]').value === 'yes';
96      monthly += cost;
97      if (critical) keep += 1;
98      else if (slow || cost <= 50) {
99        replace += 1;
100        savings += cost;
101      } else {
102        review += 1;
103      }
104    });
105
106    summary.innerHTML = `<div class="eyebrow">Your app stack cost</div><div class="stat-number">$${monthly.toLocaleString()}</div><p>/month | $${(monthly * 12).toLocaleString()} per year</p><div class="summary-stack"><div class="summary-line"><span>Keep</span><strong>${keep} apps</strong></div><div class="summary-line review"><span>Review</span><strong>${review} apps</strong></div><div class="summary-line replace"><span>Likely replaceable</span><strong>${replace} apps</strong></div></div>${savings > 0 ? `<div class="summary-saving">Potential saving if replaced: <strong>$${savings.toLocaleString()}/mo</strong> | $${(savings * 12).toLocaleString()}/yr</div>` : ''}`;
107  }
108
109  if (appRows) {
110    appRows.addEventListener('input', updateCalculator);
111    appRows.addEventListener('change', updateCalculator);
112    appRows.addEventListener('click', (event) => {
113      if (event.target.classList.contains('remove-row')) {
114        event.target.closest('.calculator-row').remove();
115        updateCalculator();
116      }
117    });
118    updateCalculator();
119  }
120  if (addRow && appRows) {
121    addRow.addEventListener('click', () => {
122      appRows.appendChild(rowTemplate());
123      updateCalculator();
124    });
125  }
126
127  const consultModal = document.querySelector('#consultation-modal');
128  if (consultModal && typeof consultModal.showModal === 'function') {
129    const body = consultModal.querySelector('[data-consult-body]');
130    let bodyDefaultHTML = body.innerHTML;
131    let modalTokenRequest = null;
132
133    function escapeAttr(value) {
134      return String(value).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
135    }
136
137    function hydrateConsultModal(data) {
138      if (!data || !data.token || !data.captcha) return;
139      const safeToken = escapeAttr(data.token);
140      const safeCaptcha = escapeAttr(data.captcha);
141      bodyDefaultHTML = bodyDefaultHTML
142        .replace(/name="public_form_token" value="[^"]*"/, `name="public_form_token" value="${safeToken}"`)
143        .replace(/<span data-captcha-question>.*?<\/span>/, `<span data-captcha-question>${safeCaptcha}</span>`);
144      const tokenInput = body.querySelector('[name="public_form_token"]');
145      const captchaQuestion = body.querySelector('[data-captcha-question]');
146      if (tokenInput) tokenInput.value = data.token;
147      if (captchaQuestion) captchaQuestion.textContent = data.captcha;
148    }
149
150    async function ensureConsultModalToken() {
151      const tokenInput = body.querySelector('[name="public_form_token"]');
152      const captchaQuestion = body.querySelector('[data-captcha-question]');
153      const hasToken = tokenInput && tokenInput.value;
154      const hasCaptcha = captchaQuestion && captchaQuestion.textContent !== 'loading...';
155      if (hasToken && hasCaptcha) return;
156      if (!modalTokenRequest) {
157        modalTokenRequest = fetch('book-consultation.php?modal_token=1', {
158          headers: { 'X-Requested-With': 'XMLHttpRequest' },
159        }).then((response) => {
160          if (!response.ok) throw new Error('Could not prepare the consultation form');
161          return response.json();
162        }).then((data) => {
163          hydrateConsultModal(data);
164          return data;
165        }).finally(() => {
166          modalTokenRequest = null;
167        });
168      }
169      await modalTokenRequest;
170    }
171
172    async function openConsultModal(trigger) {
173      if (consultModal.open) return;
174      body.innerHTML = bodyDefaultHTML;
175      document.documentElement.classList.add('modal-open');
176      try { await ensureConsultModalToken(); } catch (err) {}
177      consultModal.showModal();
178      consultModal._returnFocus = trigger || document.activeElement;
179      const firstField = consultModal.querySelector('input, textarea, select');
180      if (firstField) firstField.focus();
181    }
182
183    function closeConsultModal() {
184      if (!consultModal.open) return;
185      consultModal.close();
186    }
187
188    consultModal.addEventListener('close', () => {
189      document.documentElement.classList.remove('modal-open');
190      const returnFocus = consultModal._returnFocus;
191      if (returnFocus && typeof returnFocus.focus === 'function') returnFocus.focus();
192    });
193
194    // Clicking the backdrop (the click target is the <dialog> itself, not a
195    // descendant) closes the modal, same as pressing Escape (native).
196    consultModal.addEventListener('click', (event) => {
197      if (event.target === consultModal) closeConsultModal();
198    });
199
200    consultModal.addEventListener('click', (event) => {
201      if (event.target.closest('[data-consult-close]')) closeConsultModal();
202    });
203
204    consultModal.addEventListener('submit', async (event) => {
205      const submitForm = event.target.closest('#consultation-modal-form');
206      if (!submitForm) return;
207      event.preventDefault();
208      const errorEl = submitForm.querySelector('[data-consult-error]');
209      if (errorEl) errorEl.hidden = true;
210      const submitBtn = submitForm.querySelector('button[type="submit"]');
211      if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Sending...'; }
212      try {
213        const response = await fetch(submitForm.action, {
214          method: 'POST',
215          headers: { 'X-Requested-With': 'XMLHttpRequest' },
216          body: new FormData(submitForm),
217        });
218        const data = await response.json();
219        if (!response.ok || !data.ok) throw new Error(data.error || 'Request failed');
220        hydrateConsultModal(data);
221        body.innerHTML = '<div class="success-message"><div class="success-icon">OK</div><h2>Consultation request received.</h2><p>We will review your details and confirm a time by email or WhatsApp.</p><button class="btn btn-dark" type="button" data-consult-close>Close</button></div>';
222        const firstFocusable = body.querySelector('button, a');
223        if (firstFocusable) firstFocusable.focus();
224      } catch (err) {
225        const currentError = submitForm.querySelector('[data-consult-error]');
226        if (currentError) currentError.hidden = false;
227        if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Request a Consultation'; }
228      }
229    });
230
231    // Any link across the site that points at book-consultation.php opens the
232    // modal instead of navigating away, while staying a real link (works
233    // without JS, opens normally on ctrl/cmd/middle-click or target="_blank").
234    document.addEventListener('click', (event) => {
235      if (event.defaultPrevented || event.button !== 0) return;
236      if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
237      const link = event.target.closest('a[href]');
238      if (!link || link.target === '_blank') return;
239      let url;
240      try { url = new URL(link.href, window.location.href); } catch (e) { return; }
241      if (url.pathname.replace(/^.*\//, '') !== 'book-consultation.php' || url.hash) return;
242      event.preventDefault();
243      openConsultModal(link);
244    });
245  }
246
247  const revealItems = document.querySelectorAll('.reveal-on-scroll, .reveal-child');
248  if (revealItems.length) {
249    if (!('IntersectionObserver' in window) || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
250      revealItems.forEach((item) => item.classList.add('is-visible'));
251      return;
252    }
253
254    const observer = new IntersectionObserver((entries) => {
255      entries.forEach((entry) => {
256        if (entry.isIntersecting) {
257          entry.target.classList.add('is-visible');
258          observer.unobserve(entry.target);
259        }
260      });
261    }, { threshold: 0.14, rootMargin: '0px 0px -8% 0px' });
262
263    revealItems.forEach((item) => observer.observe(item));
264  }
265
266  document.querySelectorAll('[data-plan-control]').forEach((control) 
266=> {
267    const tabs = Array.from(control.querySelectorAll('[data-plan-tab]'));
268    const select = control.querySelector('[data-plan-select]');
269    const section = control.closest('.product-capability-section');
270    if (!section) return;
271    const grid = section.querySelector('[data-capability-grid]');
272    const context = section.querySelector('[data-plan-context]');
273    if (!grid || !context) return;
274    const cards = Array.from(grid.querySelectorAll('[data-card-plans]'));
275    const copy = {
276      nologin: 'Free Scan - no login selected. Public Shopify/headless Shopify scanning and single-product CSV move to the front.',
277      login: 'Login Free selected. Account context, search, filter, selected CSV, and currently allowed free sources move to the front.',
278      csv: '$1 CSV Source selected. CSV-capable features and supported source workflows move to the front. Direct import remains muted.',
279      direct: '$3 Direct Import selected. Draft Shopify import, queued bulk import, progress tracking, and assigned-store features move to the front.',
280      none: 'No plan selected. All cards are shown in grayscale until you choose a plan.',
281    };
282
283    function flipSort(activePlan) {
284      const first = new Map(cards.map((card) => [card, card.getBoundingClientRect()]));
285      const sorted = cards.slice().sort((a, b) => {
286        const aActive = activePlan && a.dataset.cardPlans.split(',').includes(activePlan);
287        const bActive = activePlan && b.dataset.cardPlans.split(',').includes(activePlan);
288        if (aActive !== bActive) return aActive ? -1 : 1;
289        return Number(a.style.getPropertyValue('--card-index') || 0) - Number(b.style.getPropertyValue('--card-index') || 0);
290      });
291      sorted.forEach((card) => grid.appendChild(card));
292      sorted.forEach((card) => {
293        const last = card.getBoundingClientRect();
294        const start = first.get(card);
295        const dx = start.left - last.left;
296        const dy = start.top - last.top;
297        card.animate([
298          { transform: `translate(${dx}px, ${dy}px) rotate(var(--tilt))` },
299          { transform: 'translate(0, 0) rotate(var(--tilt))' },
300        ], { duration: 420, easing: 'cubic-bezier(.2,.7,.3,1)' });
301      });
302    }
303
304    function setPlan(plan) {
305      const activePlan = plan || '';
306      tabs.forEach((tab) => tab.classList.toggle('is-active', tab.dataset.planTab === activePlan));
307      if (select) select.value = activePlan;
308      context.textContent = copy[activePlan || 'none'];
309      cards.forEach((card) => {
310        const isActive = activePlan && card.dataset.cardPlans.split(',').includes(activePlan);
311        card.classList.toggle('is-active', Boolean(isActive));
312        card.classList.toggle('is-muted', !isActive);
313      });
314      flipSort(activePlan);
315    }
316
317    tabs.forEach((tab) => tab.addEventListener('click', () => setPlan(tab.dataset.planTab)));
318    if (select) select.addEventListener('change', () => setPlan(select.value));
319    setPlan('');
320  });
321});

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.