PageSourceSearch

https://www.trend.az/assets/js/pricing_v2.js?v1782749050

js trend.az collected 2026-09-24 06:23:35 UTC 13,465 bytes, 289 lines download raw bytes

1/**
2 * pricing_v2.js — interactions for the /pricing/ page.
3 *
4 * Responsibilities:
5 *   1. Currency toggle — flip .is-azn / .is-usd / .is-eur on the plan grid
6 *      (CSS in pricing_v2.css does the price-swap show/hide) and rewrite
7 *      the CTA hrefs so /cart-preview/ opens in the same currency the
8 *      reader was looking at.
9 *   2. Business lead form — AJAX submit to pricing.php?action=business_lead,
10 *      including a reCAPTCHA v3 token with action='business_lead'. Shows
11 *      inline success / error in .pricing-v2-form-status.
12 *
13 * No jQuery — vanilla keeps the bundle small and the footprint focused.
14 *
15 * Server-side dependencies (injected by pricing.php into html_footer
16 * before this script loads):
17 *   window.RECAPTCHA_SITE_KEY — site key for grecaptcha.execute()
18 *   window.PRICING_I18N       — { errNetwork, errGeneric, successTitle,
19 *                                 successBody } — only client-rendered
20 *                                 strings; server-returned errors flow
21 *                                 back in the AJAX JSON already in the
22 *                                 current language.
23 */
24(function () {
25    'use strict';
26
27    /* ================================================================
28     * i18n bundle — populated server-side in pricing.php via
29     *     window.PRICING_I18N = { errNetwork, errGeneric,
30     *                             successTitle, successBody }
31     * Only client-only strings live here (things the JS renders without
32     * a server round-trip). Server-returned errors already arrive in the
33     * right language inside the AJAX JSON, so they don't need a fallback.
34     * The `||` chain keeps English as a safety net in case the server
35     * shipped the page without the injection (e.g. during a partial
36     * deploy).
37     * ============================================================== */
38    var I18N = (window.PRICING_I18N && typeof window.PRICING_I18N === 'object') ? window.PRICING_I18N : {};
39    function t(key, fallback) {
40        return (I18N[key] && typeof I18N[key] === 'string') ? I18N[key] : fallback;
41    }
42
43    document.addEventListener('DOMContentLoaded', function () {
44        initCurrencyToggle();
45        initBusinessReveal();
46        initBusinessForm();
47    });
48
49    /* ================================================================
50     * Business card reveal / collapse
51     *
52     * Trend for Business ships as a CTA-only card — the full contact
53     * form is hidden (via the native `hidden` attribute) until the user
54     * opts in. This keeps the pricing page visually calm: two clean
55     * Premium options on top, a single dark "Contact sales" card below.
56     *
57     * Reveal flow:
58     *   - Click #pricing-v2-business-reveal → hide intro, unhide form,
59     *     scroll form into view (block:'nearest' so we don't yank the
60     *     viewport when it's already visible), focus first input.
61     *   - Click #pricing-v2-business-cancel → inverse. Preserve whatever
62     *     the user typed so an accidental Back doesn't nuke their draft;
63     *     only clear the ephemeral status line.
64     *
65     * Handlers are no-ops if the card isn't on the page (defensive —
66     * this script might eventually be loaded on other pages).
67     * ============================================================== */
68    function initBusinessReveal() {
69        var revealBtn = document.getElementById('pricing-v2-business-reveal');
70        var cancelBtn = document.getElementById('pricing-v2-business-cancel');
71        var intro     = document.getElementById('pricing-v2-business-intro');
72        var form      = document.getElementById('pricing-v2-business-form');
73        var status    = document.getElementById('pricing-v2-business-status');
74
75        if (!revealBtn || !cancelBtn || !intro || !form) return;
76
77        revealBtn.addEventListener('click', function () {
78            intro.hidden = true;
79            form.hidden  = false;
80
81            // Focus the first field for keyboard users. Defer until after
82            // the browser has painted the un-hidden form, otherwise some
83            // engines skip the focus on an element that was display:none
84            // a tick earlier.
85            var firstField = document.getElementById('pv2-first-name');
86            window.requestAnimationFrame(function () {
87                if (firstField) firstField.focus({ preventScroll: true });
88                form.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
89            });
90        });
91
92        cancelBtn.addEventListener('click', function () {
93            form.hidden  = true;
94            intro.hidden = false;
95
96            // Clear the status line — a stale "Please enter your email"
97            // hanging around after the user dismisses and reopens the
98            // form would look like a live error.
99            if (status) {
100                status.className = 'pricing-v2-form-status';
101                status.textContent = '';
102            }
103
104            // Put focus back on the reveal button so keyboard users
105            // stay oriented (otherwise focus falls to <body>).
106            revealBtn.focus({ preventScroll: true });
107        });
108    }
109
110    /* ================================================================
111     * Currency toggle
112     *
113     * The grid (#pricing-v2-grid) carries one of:
114     *   .is-azn .is-usd .is-eur
115     * CSS uses those to show/hide the per-card .pricing-v2-price spans
116     * and the per-month hint inside the annual card. This handler just
117     * flips the class + keeps the segmented-pill active state + rewrites
118     * the CTA querystring so the downstream /cart-preview/ inherits the
119     * same choice.
120     * ============================================================== */
121    function initCurrencyToggle() {
122        var grid = document.getElementById('pricing-v2-grid');
123        var buttons = document.querySelectorAll('.pricing-v2-currency-item');
124        if (!grid || !buttons.length) return;
125
126        var CTA_SELECTOR = '.pricing-v2-cta[data-product-code]';
127        var VALID = { azn: true, usd: true, eur: true };
128
129        function apply(cur) {
130            if (!VALID[cur]) cur = 'azn';
131
132            // Grid class — drives CSS visibility of the three price spans.
133            grid.classList.remove('is-azn', 'is-usd', 'is-eur');
134            grid.classList.add('is-' + cur);
135
136            // Segmented-pill active state + a11y pressed flag.
137            buttons.forEach(function (b) {
138                var active = b.getAttribute('data-currency') === cur;
139                b.classList.toggle('is-active', active);
140                b.setAttribute('aria-pressed', active ? 'true' : 'false');
141            });
142
143            // Rewrite CTA hrefs — preserves the path (/cart-preview/P1/)
144            // but swaps the ?currency= param. Robust against future query
145            // params on the same URL (we only touch the `currency` key).
146            document.querySelectorAll(CTA_SELECTOR).forEach(function (a) {
147                var href = a.getAttribute('href') || '';
148                var split = href.split('?');
149                var path  = split[0];
150                var qs    = split[1] || '';
151
152                var params = new URLSearchParams(qs);
153                params.set('currency', cur);
154                a.setAttribute('href', path + '?' + params.toString());
155            });
156        }
157
158        buttons.forEach(function (btn) {
159            btn.addEventListener('click', function () {
160                apply(btn.getAttribute('data-currency'));
161            });
162        });
163
164        // Idempotent initial sync — if the markup ships with .is-azn already
165        // (it does — see the template) the call below is effectively a no-op,
166        // but it also ensures the CTA hrefs get rewritten if someone ever
167        // ships the page in a different default currency.
168        var initial = grid.className.match(/is-(azn|usd|eur)/);
169        apply(initial ? initial[1] : 'azn');
170    }
171
172    /* ================================================================
173     * Business lead form
174     *
175     * Submit flow:
176     *   1. Block native submit, mark button loading.
177     *   2. grecaptcha.execute() with action='business_lead' → token.
178     *   3. POST FormData (+ g-recaptcha-response) to pricing.php.
179     *   4. Render JSON response into #pricing-v2-business-status.
180     *      On success: replace the whole form with a success message.
181     *
182     * The server is also the source of truth — front-end validation here
183     * is only for rendering speed (the browser's native tooltip on required
184     * fields). Never trust it for business logic.
185     * ============================================================== */
186    function initBusinessForm() {
187        var form = document.getElementById('pricing-v2-business-form');
188        if (!form) return;
189
190        var submitBtn = document.getElementById('pricing-v2-business-submit');
191        var status    = document.getElementById('pricing-v2-business-status');
192
193        form.addEventListener('submit', function (e) {
194            e.preventDefault();
195
196            // Reset any prior state (user retrying after an error).
197            status.className = 'pricing-v2-form-status';
198            status.textContent = '';
199            submitBtn.classList.add('is-loading');
200            submitBtn.disabled = true;
201
202            // Wait for reCAPTCHA v3 to be ready before executing. Loading
203            // the api.js script is async, so on slow connections the user
204            // might hit submit before grecaptcha is defined — grecaptcha.ready
205            // handles that race condition for us.
206            if (typeof window.grecaptcha === 'undefined' || !window.RECAPTCHA_SITE_KEY) {
207                // No reCAPTCHA available (ad blocker?). Submit without a
208                // token — the server will reject and show a clear error.
209                sendForm('');
210                return;
211            }
212
213            window.grecaptcha.ready(function () {
214                window.grecaptcha
215                    .execute(window.RECAPTCHA_SITE_KEY, { action: 'business_lead' })
216                    .then(sendForm)
217                    .catch(function () {
218                        sendForm('');
219                    });
220            });
221        });
222
223        function sendForm(token) {
224            var formData = new FormData(form);
225            formData.append('action', 'business_lead');
226            if (token) formData.append('g-recaptcha-response', token);
227
228            fetch('/pricing?action=business_lead', {
229                method: 'POST',
230                body: formData,
231                credentials: 'same-origin',
232                // Prevents CSRF-surface leak via Referer for any cross-origin
233                // images/scripts that might later be embedded on the page.
234                referrerPolicy: 'same-origin'
235            })
236                .then(function (r) { return r.json(); })
237                .then(function (data) {
238                    if (data && data.success) {
239                        renderSuccess();
240                    } else {
241                        renderError((data && data.text) || t('errGeneric', 'Something went wrong. Please try again later.'));
242                    }
243                })
244                .catch(function () {
245                    renderError(t('errNetwork', 'Network error. Please check your connection and try again.'));
246                })
247                .then(function () {
248                    submitBtn.classList.remove('is-loading');
249                    submitBtn.disabled = false;
250                });
251        }
252
253        function renderError(text) {
254            status.className = 'pricing-v2-form-status is-error';
255            status.textContent = text;
256        }
257
258        function renderSuccess() {
259            // Meta Pixel Lead — the Trend-for-Business B2B inquiry submitted
260            // successfully. A qualified sales lead, distinct from a free
261            // consumer registration (CompleteRegistration). No value/currency:
262            // no synthetic lead value is assigned anywhere in this flow.
263            if (window.fbq) {
264                fbq('track', 'Lead', { content_name: 'Trend for Business', content_category: 'B2B' });
265            }
266            // Replace the form's interior with a success block. Preserves the
267            // card layout (heading + perks are siblings) so the "Contact sales"
268            // context stays visible — just the form is swapped for a thank-you.
269            // Title + body text come from window.PRICING_I18N (server-injected
270            // per current language). Using textContent on child nodes instead
271            // of innerHTML so any future translator strings can't introduce
272            // HTML by accident.
273            var wrap = form.parentNode;
274            var success = document.createElement('div');
275            success.className = 'pricing-v2-form-status is-success';
276            success.style.display = 'block';
277
278            var strong = document.createElement('strong');
279            strong.textContent = t('successTitle', 'Thanks — your message is on its way.');
280            success.appendChild(strong);
281            success.appendChild(document.createElement('br'));
282            success.appendChild(document.createTextNode(
283                t('successBody', 'Our sales team will get back to you within one working day.')
284            ));
285
286            wrap.replaceChild(success, form);
287        }
288    }
289})();

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.