PageSourceSearch

https://www.revechat.com/wp-content/themes/revechat/js/price.js?var=1.1.2

js revechat.com collected 2026-09-24 15:28:40 UTC 41,978 bytes, 1,218 lines download raw bytes

1(function ($) {
2  $(document).ready(function () {
3    function updateTicks(slider) {
4      const scale = slider.parentElement.querySelector(".scale");
5      const ticks = scale.querySelectorAll("span");
6      const totalTicks = ticks.length;
7
8      // Calculate the slider's current percentage (0 to 1)
9      const currentPercent =
10        (slider.value - slider.min) / (slider.max - slider.min);
11
12      ticks.forEach((tick, index) => {
13        // Flex 'space-between' places items at index / (total - 1)
14        const tickPercent = index / (totalTicks - 1);
15
16        // Add a tiny epsilon for floating point comparison
17        if (currentPercent >= tickPercent - 0.001) {
18          tick.classList.add("active");
19        } else {
20          tick.classList.remove("active");
21        }
22      });
23    }
24    const sliders = document.querySelectorAll('input[type="range"]');
25    function updateSliderBackground(slider) {
26      const min = slider.min;
27      const max = slider.max;
28      const val = slider.value;
29      const percentage = ((val - min) / (max - min)) * 100;
30
31      slider.style.background = `linear-gradient(to right,
32        var(--active-color) 0%,
33        var(--active-color) ${percentage}%,
34        var(--inactive-color) ${percentage}%,
35        var(--inactive-color) 100%)`;
36    }
37
38    const pricingTiers = [
39      {
40        name: "Free",
41        maxAiConversations: 10,
42        pricing: {
43          USD: {
44            basePrice: 0,
45            additionalSeatPrice: 0,
46            aiCredit: 0,
47            maxPrice: 0,
48            symbol: "$",
49          },
50          BDT: {
51            basePrice: 0,
52            additionalSeatPrice: 0,
53            aiCredit: 0,
54            maxPrice: 0,
55            symbol: "à§³",
56          },
57          INR: {
58            basePrice: 0,
59            additionalSeatPrice: 0,
60            aiCredit: 0,
61            maxPrice: 0,
62            symbol: "₹",
63          },
64        },
65        seats: 1,
66        maxSeats: 1,
67        includedChatbotSessions: 50,
68        maxChatbotSessions: 50,
69        liveChatLimit: 50,
70      },
71      {
72        name: "Starter",
73        maxAiConversations: 500,
74        pricing: {
75          USD: {
76            basePrice: 14.99,
77            additionalSeatPrice: 8.99,
78            aiCredit: 0.01,
79            maxPrice: 95.9,
80            symbol: "$",
81          },
82          BDT: {
83            basePrice: 914.39,
84            additionalSeatPrice: 549,
85            aiCredit: 1.20,
86            maxPrice: 11284.39,
87            symbol: "à§³",
88          },
89          INR: {
90            basePrice: 809.46,
91            additionalSeatPrice: 485.1,
92            aiCredit: 1.00,
93            maxPrice: 9260.46,
94            symbol: "₹",
95          },
96        },
97        seats: 1,
98        maxSeats: 5,
99        includedChatbotSessions: 500,
100        maxChatbotSessions: 2000,
101        liveChatLimit: 5000,
102      },
103      {
104        name: "Growth",
105        maxAiConversations: 10000,
106        pricing: {
107          USD: {
108            basePrice: 59.99,
109            additionalSeatPrice: 13.99,
110            aiCredit: 0.01,
111            maxPrice: 367.77,
112            symbol: "$",
113          },
114          BDT: {
115            basePrice: 3659.39,
116            additionalSeatPrice: 854,
117            aiCredit: 1.20,
118            maxPrice: 73809.39,
119            symbol: "à§³",
120          },
121          INR: {
122            basePrice: 3239.46,
123            additionalSeatPrice: 755.1,
124            aiCredit: 1.00,
125            maxPrice: 58116.96,
126            symbol: "₹",
127          },
128        },
129        seats: 3,
130        maxSeats: 25,
131        includedChatbotSessions: 4000,
132        maxChatbotSessions: 20000,
133        liveChatLimit: Infinity,
134      },
135      {
136        name: "Enterprise",
137        maxAiConversations: 150000,
138        pricing: {
139          USD: {
140            basePrice: 291.99,
141            additionalSeatPrice: 24.99,
142            aiCredit: 0.01,
143            maxPrice: Infinity,
144            symbol: "$",
145          },
146          BDT: {
147            basePrice: 17811.39,
148            additionalSeatPrice: 1525,
149            aiCredit: 1.20,
150            maxPrice: Infinity,
151            symbol: "à§³",
152          },
153          INR: {
154            basePrice: 15767.46,
155            additionalSeatPrice: 1349.1,
156            aiCredit: 1.00,
157            maxPrice: Infinity,
158            symbol: "₹",
159          },
160        },
161        seats: 10,
162        maxSeats: 200,
163        includedChatbotSessions: 10000,
164        maxChatbotSessions: 100000,
165        liveChatLimit: Infinity,
166      },
167    ];
168
169    const addonsData = {
170      "ai-credit": {
171        USD: { price: 0.25, unit: "/AI Conversation", symbol: "$" },
172        BDT: { price: 30, unit: "/AI Conversation", symbol: "à§³" },
173        INR: { price: 25, unit: "/AI Conversation", symbol: "₹" },
174      },
175      copilot: {
176        USD: { price: 9, unit: "/agent/mo", symbol: "$" },
177        BDT: { price: 900, unit: "/agent/mo", symbol: "à§³" },
178        INR: { price: 700, unit: "/agent/mo", symbol: "₹" },
179      },
180      whatsapp: {
181        USD: { price: 8, unit: "/mo", symbol: "$" },
182        BDT: { price: 488, unit: "/mo", symbol: "à§³" },
183        INR: { price: 432, unit: "/mo", symbol: "₹" },
184      },
185      sms: {
186        USD: { price: 5, unit: "/mo", symbol: "$" },
187        BDT: { price: 305, unit: "/mo", symbol: "à§³" },
188        INR: { price: 270, unit: "/mo", symbol: "₹" },
189      },
190      webpush: {
191        USD: { price: 5, unit: "/mo", symbol: "$" },
192        BDT: { price: 305, unit: "/mo", symbol: "à§³" },
193        INR: { price: 270, unit: "/mo", symbol: "₹" },
194      },
195      api: {
196        USD: { price: 0.01, unit: "/AI Credit", symbol: "$" },
197        BDT: { price: 1.20, unit: "/AI Credit", symbol: "à§³" },
198        INR: { price: 1.00, unit: "/AI Credit", symbol: "₹" },
199      },
200      youtube: {
201        USD: { price: 8, unit: "/mo", symbol: "$" },
202        BDT: { price: 488, unit: "/mo", symbol: "à§³" },
203        INR: { price: 432, unit: "/mo", symbol: "₹" },
204      },
205      telegram: {
206        USD: { price: 6, unit: "/mo", symbol: "$" },
207        BDT: { price: 366, unit: "/mo", symbol: "à§³" },
208        INR: { price: 324, unit: "/mo", symbol: "₹" },
209      },
210      viber: {
211        USD: { price: 6, unit: "/mo", symbol: "$" },
212        BDT: { price: 366, unit: "/mo", symbol: "à§³" },
213        INR: { price: 324, unit: "/mo", symbol: "₹" },
214      },
215      "google-review": {
216        USD: { price: 8, unit: "/mo", symbol: "$" },
217        BDT: { price: 488, unit: "/mo", symbol: "à§³" },
218        INR: { price: 432, unit: "/mo", symbol: "₹" },
219      },
220      "business-email": {
221        USD: { price: 5, unit: "/mo", symbol: "$" },
222        BDT: { price: 305, unit: "/mo", symbol: "à§³" },
223        INR: { price: 270, unit: "/mo", symbol: "₹" },
224      },
225      "google-play": {
226        USD: { price: 7, unit: "/mo", symbol: "$" },
227        BDT: { price: 427, unit: "/mo", symbol: "à§³" },
228        INR: { price: 378, unit: "/mo", symbol: "₹" },
229      },
230      "app-store": {
231        USD: { price: 7, unit: "/mo", symbol: "$" },
232        BDT: { price: 427, unit: "/mo", symbol: "à§³" },
233        INR: { price: 378, unit: "/mo", symbol: "₹" },
234      },
235      // whitelabel: {
236      //   USD: { price: 199, unit: "/mo", symbol: "$" },
237      //   BDT: { price: 12139, unit: "/mo", symbol: "à§³" },
238      //   INR: { price: 10746, unit: "/mo", symbol: "₹" },
239      // },
240    };
241
242    let isYearly = false; // default monthly
243    let currentCurrency = "USD";
244
245    const monthlyBtn = document.getElementById("pay-monthly");
246    const yearlyBtn = document.getElementById("pay-yearly");
247
248    monthlyBtn.addEventListener("click", () => {
249      isYearly = false;
250      monthlyBtn.classList.add("active");
251      yearlyBtn.classList.remove("active");
252      updateUI();
253    });
254
255    yearlyBtn.addEventListener("click", () => {
256      isYearly = true;
257      yearlyBtn.classList.add("active");
258      monthlyBtn.classList.remove("active");
259      updateUI();
260    });
261
262    const usersScale = [1, 3, 5, 10, 15, 20, 30, 40, 50, 100, 200];
263    const storageScale = [
264      500, 1000, 3000, 5000, 10000, 15000, 20000, 30000, 40000, 50000, 100000,
265      200000,
266    ];
267
268    function getInterpolatedValue(sliderVal, maxSliderVal, scale, step) {
269      const p = sliderVal / maxSliderVal;
270      if (p >= 1) return scale[scale.length - 1];
271      if (p <= 0) return scale[0];
272
273      const numSegments = scale.length - 1;
274      const segmentWidth = 1 / numSegments;
275      const i = Math.floor(p / segmentWidth);
276      const t = (p - i * segmentWidth) / segmentWidth;
277
278      const val = scale[i] + t * (scale[i + 1] - scale[i]);
279      return Math.round(val / step) * step;
280    }
281
282    function reverseInterpolatedValue(val, maxSliderVal, scale) {
283      // Clamp value to scale limits
284      if (val <= scale[0]) return 0;
285      if (val >= scale[scale.length - 1]) return maxSliderVal;
286
287      // Find the segment
288      let i = 0;
289      for (let j = 0; j < scale.length - 1; j++) {
290        if (val >= scale[j] && val <= scale[j + 1]) {
291          i = j;
292          break;
293        }
294      }
295
296      // Interpolate within segment
297      // val = scale[i] + t * (scale[i+1] - scale[i])
298      // t = (val - scale[i]) / (scale[i+1] - scale[i])
299      const t = (val - scale[i]) / (scale[i + 1] - scale[i]);
300
301      // p = (i + t) * segmentWidth
302      // sliderVal = p * maxSliderVal
303      const numSegments = scale.length - 1;
304      const segmentWidth = 1 / numSegments;
305      const p = (i + t) * segmentWidth;
306
307      return p * maxSliderVal;
308    }
309
310    function calculatePrice(tierIndex) {
311      const tier = pricingTiers[tierIndex];
312      const pData = tier.pricing[currentCurrency];
313      const usersSlider = document.getElementById("users");
314      const storageSlider = document.getElementById("storage");
315
316      let users = usersSlider ? getInterpolatedValue(
317        +usersSlider.value,
318        +usersSlider.max,
319        usersScale,
320        1,
321      ) : 0;
322      let sessions = storageSlider ? getInterpolatedValue(
323        +storageSlider.value,
324        +storageSlider.max,
325        storageScale,
326        1,
327      ) : 0;
328
329      // Clamp users and sessions to tier limits to stop price increase when disabled
330      users = Math.min(users, tier.maxSeats);
331      sessions = Math.min(sessions, tier.maxChatbotSessions);
332
333      const prevTier = tierIndex > 0 ? pricingTiers[tierIndex - 1] : null;
334
335      // Logic to determine effective base seats for calculation:
336      // Typically it's the tier's own base seats or the previous tier's max seats (whichever is higher).
337      // USER REQUEST: each plan will use its own "Included" seats as the fixed baseline.
338
339      let effectiveBaseSeats = tier.seats;
340
341      let effectiveBaseSessions = tier.includedChatbotSessions;
342
343      let extraSeats = Math.max(0, users - effectiveBaseSeats);
344      let extraSessions = Math.max(0, sessions - effectiveBaseSessions);
345
346      const planEl = document.getElementById(`plan-${tier.name.toLowerCase()}`);
347      let aiCreditCost = 0;
348      
349      if (planEl) {
350        // Seat inputs override the global extraSeats if present
351        const seatInput = planEl.querySelector(".seats-input");
352        if (seatInput) {
353          const baseSeats = parseFloat(seatInput.getAttribute("data-base")) || effectiveBaseSeats;
354          const currentSeats = parseFloat(seatInput.value) || baseSeats;
355          extraSeats = Math.max(0, currentSeats - baseSeats);
356        }
357
358        // AI conversation cost addition
359        const aiInput = planEl.querySelector(".ai-conversations-input");
360        if (aiInput) {
361          const baseAi = parseFloat(aiInput.getAttribute("data-base")) || 0;
362          const currentAi = parseFloat(aiInput.value) || baseAi;
363          if (currentAi > baseAi) {
364            const aiPrice = addonsData["ai-credit"][currentCurrency].price;
365            aiCreditCost = (currentAi - baseAi) * aiPrice;
366          }
367        }
368      }
369
370      let seatCost = extraSeats * pData.additionalSeatPrice;
371      let sessionCost = extraSessions * pData.aiCredit;
372
373      let total = pData.basePrice + seatCost + sessionCost + aiCreditCost;
374
375      // Apply yearly discount if selected
376      if (isYearly) {
377        // Calculate yearly total with 15% off, then show monthly equivalent
378        // (total * 12 * 0.85) / 12 = total * 0.85
379        total = total * 0.85;
380      }
381
382      return total.toLocaleString(undefined, {
383        minimumFractionDigits:
384          currentCurrency === "USD" ||
385          currentCurrency === "BDT" ||
386          currentCurrency === "INR"
387            ? 2
388            : 0,
389        maximumFractionDigits:
390          currentCurrency === "USD" ||
391          currentCurrency === "BDT" ||
392          currentCurrency === "INR"
393            ? 2
394            : 0,
395      });
396    }
397
398    function updateUI() {
399      document.querySelectorAll("[data-extra-seat-plan]").forEach((element) => {
400        const tier = pricingTiers.find((plan) => plan.name === element.dataset.extraSeatPlan);
401        if (!tier) return;
402        const pricing = tier.pricing[currentCurrency];
403        element.textContent = `${tier.name} ${pricing.symbol}${pricing.additionalSeatPrice}`;
404      });
405      const usersSlider = document.getElementById("users");
406      const storageSlider = document.getElementById("storage");
407
408      const users = usersSlider ? getInterpolatedValue(
409        +usersSlider.value,
410        +usersSlider.max,
411        usersScale,
412        1,
413      ) : 0;
414      // console.log(users);
415      const storage = storageSlider ? getInterpolatedValue(
416        +storageSlider.value,
417        +storageSlider.max,
418        storageScale,
419        1,
420      ) : 0;
421
422      const isMaxUsers = usersSlider ? +usersSlider.value >= +usersSlider.max : false;
423      const isMaxStorage = storageSlider ? +storageSlider.value >= +storageSlider.max : false;
424
425      // Update input values only if they are not the active element (to avoid jumping while typing)
426      const usersInput = document.getElementById("usersValue");
427      if (usersInput && document.activeElement !== usersInput) {
428        usersInput.value = users;
429      }
430
431      const storageInput = document.getElementById("storageValue");
432      // Format storage for input (remove K if user is typing, but we just set raw number here)
433      // Actually, for consistency let's just show the number. The user wants to change values.
434      if (storageInput && document.activeElement !== storageInput) {
435        storageInput.value = storage;
436      }
437
438      pricingTiers.forEach((tier, index) => {
439        const priceEl = document.getElementById(`price${index + 1}`);
440        const planEl = document.getElementById(
441          `plan-${tier.name.toLowerCase()}`,
442        );
443        const button = planEl.querySelector(".plan-btn"); // optional button
444
445        // Update price normally so storage works
446        const pData = tier.pricing[currentCurrency];
447        //priceEl.textContent = pData.symbol + calculatePrice(index);
448        priceEl.innerHTML = `<span class="currency-symbol">${pData.symbol}</span>` + calculatePrice(index);
449
450        const isEnterprise = tier.name === "Enterprise";
451        const isExceeded =
452          users > tier.maxSeats ||
453          storage > tier.maxChatbotSessions ||
454          (isEnterprise && (isMaxUsers || isMaxStorage));
455        // if (isExceeded) {
456        //   if (isEnterprise) {
457        //     // Enterprise stays active but changes button text and link
458        //     planEl.classList.remove("disabled");
459        //     priceEl.classList.remove("disabled");
460        //     if (button) {
461        //       button.textContent = "Contact Us";
462        //       button.setAttribute(
463        //         "href",
464        //         "https://www.revechat.com/demo-request/",
465        //       );
466        //       button.classList.remove("disabled");
467        //       button.removeAttribute("aria-disabled");
468        //       button.style.pointerEvents = "auto";
469        //       button.style.opacity = "1";
470        //     }
471        //   } else {
472        //     // Other plans get disabled
473        //     planEl.classList.add("disabled");
474        //     priceEl.classList.add("disabled");
475        //     if (button && tier.name !== "Free") {
476        //       button.classList.add("disabled");
477        //       button.setAttribute("aria-disabled", "true");
478        //       button.style.pointerEvents = "none";
479        //       button.style.opacity = "0.5";
480        //     }
481        //   }
482        // } else {
483        //   // All plans active within limits
484        //   planEl.classList.remove("disabled");
485        //   priceEl.classList.remove("disabled");
486
487        //   // Reset button to default state
488        //   if (button) {
489        //     if (isEnterprise) {
490        //       button.textContent = "Start free trial";
491        //       button.setAttribute("href", "https://www.revechat.com/signup/");
492        //     } else if (tier.name !== "Free") {
493        //       button.setAttribute("href", "https://www.revechat.com/signup/");
494        //     }
495        //     button.classList.remove("disabled");
496        //     button.removeAttribute("aria-disabled");
497        //     button.style.pointerEvents = "auto";
498        //     button.style.opacity = "1";
499        //   }
500        // }
501
502        // Update seat text if users exceed included seats
503        if (
504          tier.name === "Starter" ||
505          tier.name === "Growth" ||
506          tier.name === "Enterprise"
507        ) {
508          const seatTextEl = planEl.querySelector(".additional-seat-text");
509          if (seatTextEl) {
510            // if (users > tier.seats) {
511            //   seatTextEl.textContent = `${pData.symbol}${pData.additionalSeatPrice} per additional seat`;
512            // } else {
513            //   seatTextEl.textContent = `${pData.symbol}${pData.additionalSeatPrice} per additional seat`;
514            // }
515             if (users > tier.seats) {
516               seatTextEl.innerHTML = `<span class="currency-symbol">${pData.symbol}</span>${pData.additionalSeatPrice} per additional seat`;
517            } else {
518               seatTextEl.innerHTML = `<span class="currency-symbol">${pData.symbol}</span>${pData.additionalSeatPrice} per additional seat`;
519            }
520          }
521        }
522
523        // Update Enterprise session text if sessions exceed 10k
524        if (tier.name === "Enterprise") {
525          const sessionEl = planEl.querySelector(
526            ".pricing_features li:nth-child(2)",
527          ); // 10k chatbot sessions
528          if (sessionEl) {
529            const sessionsK = tier.includedChatbotSessions / 1000;
530            if (storage > tier.includedChatbotSessions) {
531              sessionEl.innerHTML = `${sessionsK}k chatbot sessions <span class="p_badge">Included</span> (above ${sessionsK}k)<span class="tooltip-arrow"></span>`;
532            } else {
533              sessionEl.innerHTML = `${sessionsK}k chatbot sessions <span class="p_badge">Included</span><span class="tooltip-arrow"></span>`;
534            }
535          }
536        }
537
538          // --- Update dynamic tooltips ---
539        // const cardEl = planEl.closest(".pricing_card");
540        // if (cardEl) {
541        //    const seatFeatureEl = cardEl.querySelector(".seat-feature");
542        //    if (seatFeatureEl) {
543        //       const seatLimitText = tier.name === "Enterprise" ? "Unlimited" : `Up to ${tier.maxSeats}`;
544        //       const seatPriceFormatted = pData.additionalSeatPrice.toLocaleString(undefined, {
545        //          minimumFractionDigits: pData.additionalSeatPrice % 1 !== 0 ? 2 : 0,
546        //          maximumFractionDigits: 2
547        //       });
548        //       seatFeatureEl.setAttribute("data-tooltip", `${seatLimitText} seats supported with this package. ${pData.symbol}${seatPriceFormatted} per additional seat`);
549        //    }
550
551        //    const sessionFeatureEl = cardEl.querySelector(".session-feature");
552        //    if (sessionFeatureEl) {
553        //       let sessionTooltipText = "";
554        //       if (tier.name === "Enterprise") {
555        //          sessionTooltipText = "Unlimited chatbot sessions";
556        //       } else {
557        //          const sessionsFormatted = tier.maxChatbotSessions >= 1000 ? tier.maxChatbotSessions.toLocaleString() : tier.maxChatbotSessions;
558        //          sessionTooltipText = `Up to ${sessionsFormatted} chatbot sessions`;
559        //       }
560        //       const sessionPriceFormatted = pData.aiCredit.toLocaleString(undefined, {
561        //          minimumFractionDigits: pData.aiCredit < 1 ? 3 : (pData.aiCredit % 1 !== 0 ? 2 : 0),
562        //          maximumFractionDigits: 3
563        //       });
564        //       sessionFeatureEl.setAttribute("data-tooltip", `${sessionTooltipText} with this package. ${pData.symbol}${sessionPriceFormatted} per additional chatbot session.`);
565        //    }
566        // }
567      });
568
569      // Auto-scroll to first active plan on mobile if current view has disabled plans
570      if (window.innerWidth <= 767) {
571        const firstActivePlan = document.querySelector(
572          ".pricing_card .plan:not(.disabled)",
573        );
574        if (firstActivePlan) {
575          const card = firstActivePlan.closest(".pricing_card");
576          const container = document.querySelector(".pricing_cards");
577          const cardLeft = card.offsetLeft;
578          const containerLeft = container.offsetLeft;
579
580          // Use smooth behavior for better UX
581          container.scrollTo({
582            left: cardLeft - containerLeft - 16, // 16px gap/padding adjustment if needed, or just card.offsetLeft
583            behavior: "smooth",
584          });
585        }
586      }
587
588      // Update table headers
589      document.querySelectorAll(".table-price").forEach((el) => {
590        const tierIndex = el.getAttribute("data-tier");
591        const tier = pricingTiers[tierIndex];
592        const pData = tier.pricing[currentCurrency];
593        let displayPrice = pData.basePrice;
594        if (isYearly && displayPrice > 0) {
595          displayPrice = displayPrice * 0.85;
596        }
597        // el.textContent = `from ${pData.symbol}
597${displayPrice.toLocaleString(undefined, { maximumFractionDigits: currentCurrency === "USD" || currentCurrency === "BDT" || currentCurrency === "INR" ? 2 : 0 })}/mo`;
598        el.innerHTML = `from <span class="currency-symbol">${pData.symbol}</span>${displayPrice.toLocaleString(undefined, { maximumFractionDigits: currentCurrency === "USD" || currentCurrency === "BDT" || currentCurrency === "INR" ? 2 : 0 })}/mo`;
599      });
600
601      // Update sliders visuals
602      sliders.forEach((slider) => {
603        updateSliderBackground(slider);
604        updateTicks(slider);
605      });
606
607      // Update add-ons pricing
608      Object.keys(addonsData).forEach((id) => {
609        const addonEl = document.getElementById(`addon-${id}-price`);
610        if (addonEl) {
611          const data = addonsData[id][currentCurrency];
612          const priceText = data.price.toLocaleString(undefined, {
613            minimumFractionDigits: data.price % 1 !== 0 ? 2 : 0,
614            maximumFractionDigits: 2,
615          });
616          // addonEl.innerHTML = `<p>${data.symbol}${priceText} <span>${data.unit}</span></p>`;
617           addonEl.innerHTML = `<p><span class="currency-symbol">${data.symbol}</span>${priceText} <span>${data.unit}</span></p>`;
618        }
619      });
620    }
621
622    sliders.forEach((slider) => {
623      slider.addEventListener("input", updateUI);
624      // Ensure focus is removed from inputs when interacting with sliders
625      // This allows updateUI to refresh input values on mobile
626      const blurInputs = () => {
627        const uInput = document.getElementById("usersValue");
628        const sInput = document.getElementById("storageValue");
629        if (uInput) uInput.blur();
630        if (sInput) sInput.blur();
631      };
632      slider.addEventListener("touchstart", blurInputs);
633      slider.addEventListener("mousedown", blurInputs);
634      updateTicks(slider);
635    });
636
637    updateUI();
638
639    // Add event listeners for inputs
640    const usersInputGlobal = document.getElementById("usersValue");
641    const storageInputGlobal = document.getElementById("storageValue");
642
643    function handleInputChange(input, slider, scale) {
644      if (!slider) return;
645      let val = parseInt(input.value.replace(/,/g, "")) || 0;
646      const maxVal = scale[scale.length - 1];
647
648      // Clamp value
649      if (val < scale[0]) val = scale[0];
650      if (val > maxVal) val = maxVal;
651
652      // Update input field if it was clamped
653      if (parseInt(input.value.replace(/,/g, "")) !== val) {
654        input.value = val;
655      }
656
657      // Update slider position with high precision
658      const sliderVal = reverseInterpolatedValue(val, +slider.max, scale);
659
660      // Update slider and trigger visuals
661      slider.value = sliderVal;
662      updateUI();
663      updateSliderBackground(slider);
664      updateTicks(slider);
665    }
666
667    if (usersInputGlobal) {
668      usersInputGlobal.addEventListener("change", (e) => {
669        handleInputChange(
670          usersInputGlobal,
671          document.getElementById("users"),
672          usersScale,
673        );
674      });
675
676      usersInputGlobal.addEventListener("input", () => {
677        // Don't fully handle on every keystroke as it might be mid-number
678        // but we can update the slider if it's a valid number
679        let val = parseInt(usersInputGlobal.value.replace(/,/g, ""));
680        if (!isNaN(val)) {
681          const maxVal = usersScale[usersScale.length - 1];
682          if (val > maxVal) {
683            val = maxVal;
684            usersInputGlobal.value = val;
685          }
686
687          const slider = document.getElementById("users");
688          if (slider) {
689            const sliderVal = reverseInterpolatedValue(
690              val,
691              +slider.max,
692              usersScale,
693            );
694            slider.value = sliderVal;
695            updateUI();
696            updateSliderBackground(slider);
697            updateTicks(slider);
698          }
699        }
700      });
701
702      usersInputGlobal.addEventListener("keypress", (e) => {
703        if (e.key === "Enter") {
704          e.preventDefault();
705          handleInputChange(
706            usersInputGlobal,
707            document.getElementById("users"),
708            usersScale,
709          );
710          usersInputGlobal.blur();
711        }
712      });
713    }
714
715    if (storageInputGlobal) {
716      storageInputGlobal.addEventListener("change", () => {
717        handleInputChange(
718          storageInputGlobal,
719          document.getElementById("storage"),
720          storageScale,
721        );
722      });
723
724      storageInputGlobal.addEventListener("input", () => {
725        let val = parseInt(storageInputGlobal.value.replace(/,/g, ""));
726        if (!isNaN(val)) {
727          const maxVal = storageScale[storageScale.length - 1];
728          if (val > maxVal) {
729            val = maxVal;
730            storageInputGlobal.value = val;
731          }
732          // if (val < storageScale[0]) val = storageScale[0]; // Optional: don't clamp min while typing
733
734          const slider = document.getElementById("storage");
735          if (slider) {
736            const sliderVal = reverseInterpolatedValue(
737              val,
738              +slider.max,
739              storageScale,
740            );
741            slider.value = sliderVal;
742            updateUI();
743            updateSliderBackground(slider);
744            updateTicks(slider);
745          }
746        }
747      });
748
749      storageInputGlobal.addEventListener("keypress", (e) => {
750        if (e.key === "Enter") {
751          e.preventDefault();
752          handleInputChange(
753            storageInputGlobal,
754            document.getElementById("storage"),
755            storageScale,
756          );
757          storageInputGlobal.blur();
758        }
759      });
760    }
761
762    const mainTable = document.getElementById("main-table");
763    const stickyHeader = document.getElementById("sticky-header");
764    const mainHeader = mainTable.querySelector("thead");
765    // const pricingSidebar = document.querySelector("pricing-section-heading");
766   // const pricingSidebar = document.querySelector(".pricing-section-heading");
767
768    const lastTable = document.querySelector(".last-table");
769    const showMoreContainer = document.getElementById(
770      "showMoreFeaturesContainer",
771    );
772    const extraFeatures = document.getElementById("extraFeaturesContainer");
773
774    const pricingWrapper = document.querySelector(".pricing-table-wrapper");
775
776    function matchWidths() {
777      const mainThs = mainHeader.querySelectorAll("th");
778      const stickyThs = stickyHeader.querySelectorAll("th");
779
780      mainThs.forEach((th, i) => {
781        stickyThs[i].style.width = th.getBoundingClientRect().width + "px";
782      });
783
784      stickyHeader.style.width = mainTable.getBoundingClientRect().width + "px";
785    }
786
787    function syncStickyHorizontalScroll() {
788      if (pricingWrapper && stickyHeader) {
789        const scrollLeft = pricingWrapper.scrollLeft;
790        stickyHeader.style.transform = `translateX(${-scrollLeft}px)`;
791      }
792    }
793
794    window.addEventListener("scroll", () => {
795      const headerTop = mainHeader.getBoundingClientRect().top;
796
797      // Determine the bottom boundary: last-table if visible, otherwise show-more button container
798      const isExtraVisible =
799        extraFeatures && extraFeatures.style.display !== "none";
800      const bottomElement = isExtraVisible ? lastTable : showMoreContainer;
801      const bottomTop = bottomElement
802        ? bottomElement.getBoundingClientRect().top
803        : 0;
804
805      if (headerTop <= 0 && bottomTop > 0) {
806        stickyHeader.style.display = "table";
807       // pricingSidebar.style.display = "none";
808        matchWidths();
809        syncStickyHorizontalScroll(); // Sync horizontal scroll when it appears
810      } else {
811        stickyHeader.style.display = "none";
812      //  pricingSidebar.style.display = "block";
813      }
814    });
815
816    if (pricingWrapper) {
817      pricingWrapper.addEventListener("scroll", syncStickyHorizontalScroll);
818    }
819
820    // Update widths on resize
821    window.addEventListener("resize", () => {
822      if (stickyHeader.style.display === "table") {
823        matchWidths();
824      }
825    });
826
827    const nav = document.querySelector(".pay_yearly_or_mon ul");
828    const indicator = document.createElement("div");
829    indicator.classList.add("nav-indicator");
830    nav.appendChild(indicator);
831
832    // Update indicator position
833    function updateIndicator(activeBtn) {
834      const rect = activeBtn.getBoundingClientRect();
835      const navRect = nav.getBoundingClientRect();
836      indicator.style.width = rect.width + "px";
837      indicator.style.left = rect.left - navRect.left + nav.scrollLeft + "px";
838    }
839
840    // Initial active button
841    const active = nav.querySelector("button.active");
842    if (active) updateIndicator(active);
843
844    // Click events on buttons
845    nav.querySelectorAll("button").forEach((btn) => {
846      btn.addEventListener("click", () => {
847        // Remove active from others
848        nav
849          .querySelectorAll("button")
850          .forEach((b) => b.classList.remove("active"));
851        btn.classList.add("active");
852
853        updateIndicator(btn);
854
855        // Update pricing mode
856        isYearly = btn.id === "pay-yearly";
857        updateUI();
858      });
859    });
860
861    // Update indicator on scroll
862    nav.addEventListener("scroll", () => {
863      const active = nav.querySelector("button.active");
864      if (active) updateIndicator(active);
865    });
866
867    // Optional: update indicator on window resize
868    window.addEventListener("resize", () => {
869      const active = nav.querySelector("button.active");
870      if (active) updateIndicator(active);
871    });
872
873    // --- Currency Switcher Logic ---
874    const currencyBtns = document.querySelectorAll(".currency-btn");
875    currencyBtns.forEach((btn) => {
876      btn.addEventListener("click", () => {
877        currentCurrency = btn.getAttribute("data-currency");
878        currencyBtns.forEach((b) => b.classList.remove("active"));
879        btn.classList.add("active");
880        updateUI();
881      });
882    });
883
884    // --- Location Auto-Switch Logic ---
885    window.geoInfoCallback = function (data) {
886      if (data && data.status === "success" && data.result) {
887        const countryCode = data.result.country_code;
888        if (countryCode === "BD") {
889          setCurrency("BDT");
890        } else if (countryCode === "IN") {
891          setCurrency("INR");
892        } else {
893          setCurrency("USD");
894        }
895        const params = new URLSearchParams(window.location.search);
896        const currency = params.get("currency");
897
898        if (currency) {
899          const curr = currency.toLowerCase();
900
901          if (curr === "usd") {
902            setCurrency("USD");
903          } else if (curr === "inr") {
904            setCurrency("INR");
905          } else if (curr === "bd" || curr === "bdt") {
906            setCurrency("BDT");
907          }
908        }
909
910      }
911    };
912
913    function setCurrency(cur) {
914      currentCurrency = cur;
915      currencyBtns.forEach((btn) => {
916        if (btn.getAttribute("data-currency") === cur) {
917          btn.classList.add("active");
918        } else {
919          btn.classList.remove("active");
920        }
921      });
922      updateUI();
923    }
924
925    // Fetch Location via REVE Chat API (using JSONP to bypass CORS)
926    $.ajax({
927      url: "https://location.revechat.com:9090/",
928      type: "get",
929      dataType: "jsonp",
930      jsonp: "callback",
931      jsonpCallback: "geoInfoCallback",
932    }).fail(function () {
933      // console.log("Location detection failed, defaulting to USD");
934    });
935
936   const links = document.querySelectorAll(".pricing-section-heading a");
937
938  const mobileOffset = 70;
939
940  links.forEach((link) => {
941    link.addEventListener("click", function (e) {
942      e.preventDefault();
943
944      const targetId = this.getAttribute("href").substring(1);
945      const section = document.getElementById(targetId);
946
947      if (!section) return;
948
949      const offset = window.innerWidth <= 768 ? mobileOffset : 0;
950
951      window.scrollTo({
952        top: section.offsetTop - offset,
953        behavior: "smooth",
954      });
955    });
956  });
957
958  function onScroll() {
959    const offset = window.innerWidth <= 768 ? mobileOffset : 0;
960
961    // Current visible position with offset
962    const scrollPosition = window.scrollY + offset + 10;
963
964    let activeLink = null;
965
966    links.forEach((link) => {
967      const targetId = link.getAttribute("href").substring(1);
968      const section = document.getElementById(targetId);
969
970      if (!section) return;
971
972      const top = section.offsetTop;
973      const bottom = top + section.offsetHeight;
974
975      if (scrollPosition >= top && scrollPosition < bottom) {
976        activeLink = link;
977      }
978    });
979
980    links.forEach((link) => {
981      link.classList.remove("highlighted");
982    });
983
984    if (activeLink) {
985      activeLink.classList.add("highlighted");
986    }
987  }
988
989  window.addEventListener("scroll", onScroll);
990
991  // Initial check
992  onScroll();
993
994    // --- Features Accordion Toggle Logic ---
995    const showMoreBtn = document.getElementById("showMoreFeaturesBtn");
996    const showLessBtn = document.getElementById("showLessFeaturesBtn");
997
998    if (showMoreBtn && extraFeatures) {
999      showMoreBtn.addEventListener("click", function () {
1000        extraFeatures.style.display = "block";
1001        if (showMoreContainer) showMoreContainer.style.display = "none";
1002      });
1003    }
1004
1005    if (showLessBtn && extraFeatures) {
1006      showLessBtn.addEventListener("click", function () {
1007        extraFeatures.style.display = "none";
1008        if (showMoreContainer) {
1009          showMoreContainer.style.display = "block";
1010          // Scroll back up to the "See more" position so the user isn't lost
1011          showMoreContainer.scrollIntoView({
1012            behavior: "smooth",
1013            block: "center",
1014          });
1015        }
1016      });
1017    }
1018
1019    // --- Counter Box Logic ---
1020   function getValue(input) {
1021      return parseInt(String(input.value).replace(/[^0-9]/g, "")) || 0;
1022    }
1023
1024    // -------------------------
1025    // RENDER VALUE SAFELY
1026    // -------------------------
1027     function renderValue(input, value) {
1028      const min = parseInt(input.getAttribute("min")) || 0;
1029      const max = parseInt(input.getAttribute("max")) || Infinity;
1030
1031      let v = value;
1032
1033      if (v < min) v = min;
1034      if (v > max) v = max;
1035
1036      input.dataset.value = v;
1037
1038      input.value = v;
1039      input.classList.toggle("has-label", v === min);
1040
1041      const inclSpan = input.closest(".input-wrapper")?.querySelector(".incl");
1042      if (inclSpan) {
1043        inclSpan.style.display = input.classList.contains("has-label")
1044          ? "inline"
1045          : "none";
1046      }
1047      syncButtons(input);
1048    }
1049
1050    // -------------------------
1051    // ENABLE / DISABLE BUTTONS
1052    // -------------------------
1053   function syncButtons(input) {
1054      const planEl = input.closest(".plan");
1055      if (!planEl) return;
1056
1057      const counterBox = input.closest(".counter-box");
1058
1059      const minus = counterBox.querySelector(".minus");
1060      const plus = counterBox.querySelector(".plus");
1061
1062      const button = planEl.querySelector(".plan-btn");
1063      const priceEl = planEl.querySelector(".price");
1064
1065      const min = parseInt(input.getAttribute("min")) || 0;
1066      const max = parseInt(input.getAttribute("max")) || Infinity;
1067
1068      const current = getValue(input);
1069
1070      const isEnterprise = planEl.classList.contains("plan-enterprise");
1071
1072      // Counter buttons
1073      minus.disabled = current <= min;
1074      plus.disabled = current >= max;
1075
1076      const isExceeded = current >= max;
1077
1078      if (isExceeded) {
1079        if (isEnterprise) {
1080          if (button) {
1081            button.textContent = "Contact Us";
1082            button.setAttribute(
1083              "href",
1084              "https://www.revechat.com/demo-request/"
1085            );
1086          }
1087        } 
1088      } 
1089      else {
1090        if (button) {
1091          if (isEnterprise) {
1092            button.textContent = "Start free trial";
1093          }
1094
1095          button.setAttribute(
1096            "href",
1097            "https://www.revechat.com/signup/"
1098          );
1099        }
1100      }
1101    }
1102
1103    // -------------------------
1104    // CORE UPDATE LOGIC
1105    // -------------------------
1106    function getCounterStep(input, direction, current) {
1107      if (!input.hasAttribute("data-progressive-steps")) {
1108        return parseInt(input.getAttribute("step")) || 1;
1109      }
1110
1111      const tiers = input.closest(".plan")?.id === "plan-growth"
1112        ? [[5000, 250], [10000, 500]]
1113        : [[10000, 1000], [25000, 2500], [50000, 5000], [150000, 10000]];
1114
1115      for (const [limit, step] of tiers) {
1116        if (direction === "plus" ? current < limit : current <= limit) {
1117          return step;
1118        }
1119      }
1120      return tiers[tiers.length - 1][1];
1121    }
1122
1123    function update(input, direction) {
1124      const current = getValue(input);
1125      const step = getCounterStep(input, direction, current);
1126
1127      const next = direction === "plus"
1128        ? current + step
1129        : current - step;
1130
1131      renderValue(input, next);
1132
1133      // IMPORTANT: ONE SINGLE UI UPDATE
1134      updateUI();
1135    }
1136
1137    // -------------------------
1138    // CLICK HANDLER (ONLY ONE)
1139    // -------------------------
1140    let holdTimeout;
1141    let holdInterval;
1142
1143    function startHold(button) {
1144      stopHold();
1145
1146      const box = button.closest(".counter-box");
1147      const input = box.querySelector(".counter-input");
1148
1149      if (!input) return;
1150      // let inclSpan = box.querySelector('.incl');
1151      // if (inclSpan) {
1152      //   inclSpan.remove();
1153      // }
1154
1155      const action = button.classList.contains("plus")
1156        ? "plus"
1157        : "minus";
1158
1159      // First update immediately
1160      update(input, action);
1161
1162      // Start repeating after delay
1163      holdTimeout = setTimeout(() => {
1164        holdInterval = setInterval(() => {
1165          update(input, action);
1166        }, 80);
1167      }, 350);
1168    }
1169
1170    function stopHold() {
1171      clearTimeout(holdTimeout);
1172      clearInterval(holdInterval);
1173
1174      holdTimeout = null;
1175      holdInterval = null;
1176    }
1177
1178    $(document)
1179
1180      .on("pointerdown", ".counter-btn", function (e) {
1181        e.preventDefault();
1182        startHold(this);
1183      })
1184
1185      .on(
1186        "pointerup pointerleave pointercancel",
1187        ".counter-btn",
1188        function () {
1189          stopHold();
1190        }
1191      );
1192
1193    // -------------------------
1194    // MANUAL INPUT (SAFE)
1195    // -------------------------
1196    $(document).on("input blur", ".counter-input", function () {
1197      const value = getValue(this);
1198      renderValue(this, value);
1199      updateUI();
1200    });
1201
1202    // -------------------------
1203    // INIT
1204    // -------------------------
1205    pricingTiers.forEach((tier) => {
1206      const input = document.querySelector(
1207        `#plan-${tier.name.toLowerCase()} .ai-conversations-input`
1208      );
1209      if (input) input.setAttribute("max", tier.maxAiConversations);
1210    });
1211
1212    $(".counter-input").each(function () {
1213      renderValue(this, getValue(this));
1214    });
1215 
1216
1217  });
1218})(jQuery);

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.