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.