1/* 2 IFRAME AUTO-RESIZE 3 ------------------ 4 Measures the calculator content itself, rather than the iframe viewport. 5 This allows the iframe to shrink from its initial fallback height and grow 6 whenever results, messages, fonts, or responsive layout change. 7*/ 8function getCalculatorContentHeight() { 9 const shell = document.querySelector(".solar-shell"); 10 if (!shell) return 0; 11 12 const shellRect = shell.getBoundingClientRect(); 13 const bodyStyles = window.getComputedStyle(document.body); 14 const paddingBottom = parseFloat(bodyStyles.paddingBottom) || 0; 15 16 return Math.ceil(shellRect.bottom + paddingBottom); 17} 18 19let lastSentHeight = 0; 20let resizeFrame = null; 21 22function sendCalculatorHeight() { 23 if (resizeFrame) cancelAnimationFrame(resizeFrame); 24 25 resizeFrame = requestAnimationFrame(function() { 26 const height = getCalculatorContentHeight(); 27 if (!height || Math.abs(height - lastSentHeight) < 2) return; 28 29 lastSentHeight = height; 30 window.parent.postMessage({ 31 type: "riverside-calc-resize", 32 height: height 33 }, "*"); 34 }); 35} 36 37window.addEventListener("load", sendCalculatorHeight); 38window.addEventListener("resize", sendCalculatorHeight); 39window.addEventListener("transitionend", sendCalculatorHeight); 40window.addEventListener("animationend", sendCalculatorHeight); 41 42if ("ResizeObserver" in window) { 43 const resizeObserver = new ResizeObserver(sendCalculatorHeight); 44 const shell = document.querySelector(".solar-shell"); 45 if (shell) resizeObserver.observe(shell); 46} 47 48if ("MutationObserver" in window) { 49 const mutationObserver = new MutationObserver(sendCalculatorHeight); 50 mutationObserver.observe(document.body, { 51 childList: true, 52 subtree: true, 53 attributes: true, 54 characterData: true 55 }); 56} 57 58if (document.fonts && document.fonts.ready) { 59 document.fonts.ready.then(sendCalculatorHeight); 60} 61 62setTimeout(sendCalculatorHeight, 100); 63setTimeout(sendCalculatorHeight, 300); 64setTimeout(sendCalculatorHeight, 750); 65setTimeout(sendCalculatorHeight, 1500); 66 67 68const assumptions = { 69 residentialHydroRate: 0.125, 70 smallGeneralServiceRate: 0.15, 71 mediumGeneralServiceRate: 0.10, 72 largeGeneralServiceRate: 0.07, 73 hydroInflationRate: 0.04, 74 lifetimeYears: 25, 75 panelWattage: 500, 76 productionPerKw: 1140, 77 financingYears: 15, 78 minSystemKw: 3, 79 maxSystemKw: 16 80 }; 81 82 83 const pvWattsConfig = { 84 usePVWatts: true, 85 nrelApiKey: "cWUPlScq3DdesZtTKogmZebndxEEyXB5UmFdWuAU", 86 defaultTilt: 30, 87 defaultAzimuth: 180, 88 defaultLosses: 10, 89 moduleType: 0, 90 arrayType: 1 91 }; 92 93 94 const makeConfig = { 95 usageWebhookUrl: "https://hook.eu2.make.com/79qxcenn4nxg2tgyuqw1jp7gpm8738ua", 96 leadWebhookUrl: "https://hook.eu2.make.com/79qxcenn4nxg2tgyuqw1jp7gpm8738ua" 97 }; 98 99 let latestEstimate = null; 100 101 function hasWebhook(url) { 102 return url && url.startsWith("https://"); 103 } 104 105 async function sendToMake(webhookUrl, payload) { 106 if (!hasWebhook(webhookUrl)) { 107 console.log("Make webhook not configured yet.", payload); 108 return { skipped: true }; 109 } 110 111 const response = await fetch(webhookUrl, { 112 method: "POST", 113 headers: { 114 "Content-Type": "application/json" 115 }, 116 body: JSON.stringify(payload) 117 }); 118 119 if (!response.ok) { 120 throw new Error("Make webhook failed with status " + response.status); 121 } 122 123 return { ok: true }; 124 } 125 126 function getInputSnapshot() { 127 return { 128 cityOrTown: document.getElementById("address")?.value || "", 129 monthlyBill: Number(document.getElementById("monthlyBill")?.value || 0), 130 propertyType: document.getElementById("propertyType")?.value || "", 131 paymentOption: document.getElementById("financingOption")?.selectedOptions?.[0]?.text || "", 132 pageUrl: window.location.href, 133 submittedAt: new Date().toISOString() 134 }; 135 } 136 137 async function trackCalculatorUsage() { 138 if (!latestEstimate) return; 139 140 const payload = { 141 eventType: "calculator_usage", 142 source: "Riverside Website Solar Savings Estimator", 143 inputs: getInputSnapshot(), 144 estimate: latestEstimate 145 }; 146
147 try { 148 await sendToMake(makeConfig.usageWebhookUrl, payload); 149 } catch (error) { 150 console.warn("Usage tracking failed:", error); 151 } 152 } 153 154 function setLeadMessage(message, isWarning = false) { 155 const messageBox = document.getElementById("leadSubmitMessage"); 156 if (!messageBox) return; 157 158 messageBox.innerText = message; 159 messageBox.classList.toggle("warning", isWarning); 160 messageBox.style.display = "block"; 161 sendCalculatorHeight(); 162 } 163 164 async function submitFullReportLead() { 165 const fullAddress = document.getElementById("leadAddress")?.value.trim(); 166 const name = document.getElementById("leadName")?.value.trim(); 167 const email = document.getElementById("leadEmail")?.value.trim(); 168 const phone = document.getElementById("leadPhone")?.value.trim(); 169 const button = document.getElementById("fullReportButton"); 170 171 if (!fullAddress || !name || !email || !phone) { 172 setLeadMessage("Please complete your full address, name, email, and phone number so we can prepare your report.", true); 173 return; 174 } 175 176 if (!latestEstimate) { 177 setLeadMessage("Please calculate your solar estimate first, then request the full report.", true); 178 return; 179 } 180 181 const payload = { 182 eventType: "full_report_request", 183 source: "Riverside Website Solar Savings Estimator", 184 contact: { 185 name, 186 email, 187 phone, 188 fullAddress 189 }, 190 inputs: getInputSnapshot(), 191 estimate: latestEstimate, 192 pageUrl: window.location.href, 193 submittedAt: new Date().toISOString() 194 }; 195 196 try { 197 if (button) button.innerText = "Sending Your Request..."; 198 await sendToMake(makeConfig.leadWebhookUrl, payload); 199 setLeadMessage("Your request has been received. Weâll review the details and prepare your full report within 1â48 hours."); 200 if (button) button.innerText = "Request Received"; 201 } catch (error) { 202 console.warn("Lead submission failed:", error); 203 setLeadMessage("Something went wrong while sending your request. Please call Riverside or try again in a moment.", true); 204 if (button) button.innerText = "Generate My Full Report"; 205 } 206 } 207 208 209 210 function showPVStatus(message, isWarning = false) { 211 const statusBox = document.getElementById("pvStatus"); 212 if (!statusBox) return; 213 214 statusBox.innerText = message; 215 statusBox.classList.toggle("warning", isWarning); 216 statusBox.style.display = "block"; 217 sendCalculatorHeight(); 218 } 219 220 function hidePVStatus() { 221 const statusBox = document.getElementById("pvStatus"); 222 if (!statusBox) return; 223 224 statusBox.style.display = "none"; 225 statusBox.classList.remove("warning"); 226 statusBox.innerText = ""; 227 sendCalculatorHeight(); 228 } 229 function getCityDisplayName(locationDisplayName) { 230 if (!locationDisplayName) return "your local area"; 231 const parts = locationDisplayName.split(",").map(function(part) { 232 return part.trim(); 233 }); 234 235 return parts.slice(0, 3).join(", "); 236 } 237 238 function updateLocationEstimateCard(pvWattsUsed, locationDisplayName, productionPerKwUsed, fallbackReason) { 239 const textEl = document.getElementById("locationEstimateText"); 240 if (!textEl) return; 241 242 if (pvWattsUsed) { 243 const cityName = getCityDisplayName(locationDisplayName); 244 textEl.innerText = "This estimate uses historical solar resource data for " + cityName + ". Production used: " + Math.round(productionPerKwUsed).toLocaleString() + " kWh per installed kW per year."; 245 } else { 246 textEl.innerText = "We could not complete the location-specific lookup, so this estimate uses Riverside's regional production assumption of " + Math.round(productionPerKwUsed).toLocaleString() + " kWh per installed kW per year."; 247 } 248 } 249 250 function renderMonthlyProductionChart(monthlyProduction) { 251 const chart = document.getElementById("monthlyChart"); 252 if (!chart) return; 253 254 chart.innerHTML = ""; 255 256 if (!monthlyProduction || monthlyProduction.length === 0) { 257 chart.innerHTML = "<p>Monthly production data was not available for this estimate.</p>"; 258 return; 259 } 260 261 const monthLabels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 262 const maxValue = Math.max.apply(null, monthlyProduction); 263 264 monthlyProduction.forEach(function(value, index) {
265 const row = document.createElement("div"); 266 row.className = "monthly-row"; 267 268 const label = document.createElement("div"); 269 label.className = "monthly-label"; 270 label.innerText = monthLabels[index] || ("M" + (index + 1)); 271 272 const track = document.createElement("div"); 273 track.className = "monthly-bar-track"; 274 275 const bar = document.createElement("div"); 276 bar.className = "monthly-bar"; 277 bar.style.width = Math.max((value / maxValue) * 100, 3) + "%"; 278 279 const number = document.createElement("div"); 280 number.className = "monthly-value"; 281 number.innerText = Math.round(value).toLocaleString() + " kWh"; 282 283 track.appendChild(bar); 284 row.appendChild(label); 285 row.appendChild(track); 286 row.appendChild(number); 287 chart.appendChild(row); 288 }); 289 } 290 291 async function geocodeAddress(address) { 292 const url = "https://nominatim.openstreetmap.org/search?format=json&limit=1&countrycodes=ca&q=" + encodeURIComponent(address); 293 294 const response = await fetch(url, { 295 headers: { 296 "Accept": "application/json" 297 } 298 }); 299 300 if (!response.ok) { 301 throw new Error("Address lookup failed."); 302 } 303 304 const data = await response.json(); 305 306 if (!data || data.length === 0) { 307 throw new Error("We could not find that city or town."); 308 } 309 310 return { 311 lat: Number(data[0].lat), 312 lon: Number(data[0].lon), 313 displayName: data[0].display_name 314 }; 315 } 316 317 async function getPVWattsAnnualProduction(address, systemSizeKw) { 318 const location = await geocodeAddress(address); 319 320 const params = new URLSearchParams({ 321 api_key: pvWattsConfig.nrelApiKey, 322 lat: location.lat, 323 lon: location.lon, 324 system_capacity: systemSizeKw, 325 module_type: pvWattsConfig.moduleType, 326 losses: pvWattsConfig.defaultLosses, 327 array_type: pvWattsConfig.arrayType, 328 tilt: pvWattsConfig.defaultTilt, 329 azimuth: pvWattsConfig.defaultAzimuth, 330 timeframe: "monthly", 331 radius: 0 332 }); 333 334 const url = "https://developer.nlr.gov/api/pvwatts/v8.json?" + params.toString(); 335 const response = await fetch(url); 336 337 if (!response.ok) { 338 throw new Error("PVWatts request failed."); 339 } 340 341 const data = await response.json(); 342 343 if (data.errors && data.errors.length > 0) { 344 throw new Error(data.errors.join(" ")); 345 } 346 347 if (!data.outputs || typeof data.outputs.ac_annual !== "number") { 348 throw new Error("PVWatts did not return annual production."); 349 } 350 351 return { 352 annualProduction: data.outputs.ac_annual, 353 monthlyProduction: data.outputs.ac_monthly || [], 354 location: location 355 }; 356 } 357 358 359 360 function formatMoney(value) { 361 return Math.round(value).toLocaleString(); 362 } 363 364 function getCostPerWatt(systemSize) { 365 if (systemSize <= 3) return 2.80; 366 if (systemSize <= 4) return 2.70; 367 if (systemSize <= 9) return 2.50; 368 if (systemSize <= 14) return 2.40; 369 return 2.30; 370 } 371 372 function getRebate(systemSize, propertyType) { 373 if (propertyType !== "Residential") { 374 return Math.min(systemSize * 1000, 10000); 375 } 376 377 return Math.min(systemSize * 1000, 5000); 378 } 379 380 function monthlyLoanPayment(principal, annualRate, years) { 381 const months = years * 12; 382 383 if (annualRate === 0) { 384 return principal / months; 385 } 386 387 const monthlyRate = annualRate / 12; 388 return principal * (monthlyRate * Math.pow(1 + monthlyRate, months)) / (Math.pow(1 + monthlyRate, months) - 1); 389 } 390 391 function getHydroRateForPropertyType(propertyType) { 392 if (propertyType === "Small General Service") return assumptions.smallGeneralServiceRate; 393 if (propertyType === "Medium General Service") return assumptions.mediumGeneralServiceRate; 394 if (propertyType === "Large General Service") return assumptions.largeGeneralServiceRate; 395 return assumptions.residentialHydroRate; 396 } 397 398 function updateFinancingOptions() { 399 const propertyType = document.getElementById("propertyType").value; 400 const financingOption = document.getElementById("financingOption"); 401 const financingHelper = document.getElementById("financingHelper"); 402 403 financingOption.innerHTML = ""; 404 405 if (propertyType === "Residential") { 406 financingOption.innerHTML = ` 407 <option value="vancity" selected>VanCity Planet Wise 5.2%</option> 408 <option value="cash">Cash</option> 409 `; 410 financingHelper.innerText = "Residential financing uses the VanCity Planet Wise assumption. Subject to approval and future rate changes."; 411 } else { 412 financingOption.innerHTML = ` 413 <option value="business_financing" selected>Financing</option> 414 <option value="cash">Cash</option> 415 `; 416 financingHelper.innerText = "Business financing uses an 8% estimate in the calculation. Subject to approval, lender terms, and future rate changes."; 417 } 418 } 419 420 function getSelectedFinancingRate() { 421 const selectedOption = document.getElementById("financingOption").value; 422 423 if (selectedOption === "cash") return 0; 424 if (selectedOption === "vancity") return 0.052; 425 if (selectedOption === "business_financing") return 0.08; 426 427 return 0; 428 } 429 430 function getSelectedFinancingLabel() { 431 const selectedOption = document.getElementById("financingOption").value; 432 433 if (selectedOption === "cash") return "Cash Purchase"; 434 if (selectedOption === "vancity") return "VanCity Planet Wise 5.2%"; 435 if (selectedOption === "business_financing") return "Business Financing"; 436 437 return "Payment Option"; 438 } 439 440 async function calculateSolar() { 441 const monthlyBill = Number(document.getElementById("monthlyBill").value); 442 const address = document.getElementById("address").value.trim(); 443 const propertyType = document.getElementById("propertyType").value; 444 const hydroRate = getHydroRateForPropertyType(propertyType); 445 const taxRate = 0.28; 446 const financingRate = getSelectedFinancingRate(); 447 const financingLabel = getSelectedFinancingLabel(); 448 449 const errorBox = document.getElementById("errorBox"); 450 451 if (!monthlyBill || monthlyBill <= 0 || !address) { 452 errorBox.style.display = "block"; 453 document.getElementById("solarResults").style.display = "none"; 454 return; 455 } 456 457 errorBox.style.display = "none"; 458 showPVStatus("Calculating local solar production for your area..."); 459 460 const annualHydroSpend = monthlyBill * 12; 461 const estimatedAnnualKwh = annualHydroSpend / hydroRate; 462 463 let suggestedSize = estimatedAnnualKwh / assumptions.productionPerKw; 464 suggestedSize = Math.round(suggestedSize); 465 suggestedSize = Math.max(assumptions.minSystemKw, suggestedSize); 466 suggestedSize = Math.min(assumptions.maxSystemKw, suggestedSize); 467 468 const costPerWatt = getCostPerWatt(suggestedSize); 469 const systemCost = suggestedSize * 1000 * costPerWatt; 470 const rebate = getRebate(suggestedSize, propertyType); 471 const netCost = Math.max(systemCost - rebate, 0); 472 let annualProduction = suggestedSize * assumptions.productionPerKw; 473 let monthlyProduction = []; 474 let pvWattsUsed = false;
475 let pvWattsLocation = ""; 476 let productionPerKwUsed = assumptions.productionPerKw; 477 let pvFailureReason = ""; 478 479 if (pvWattsConfig.usePVWatts) { 480 try { 481 const pvWattsResult = await getPVWattsAnnualProduction(address, suggestedSize); 482 annualProduction = pvWattsResult.annualProduction; 483 monthlyProduction = pvWattsResult.monthlyProduction; 484 productionPerKwUsed = annualProduction / suggestedSize; 485 pvWattsUsed = true; 486 pvWattsLocation = pvWattsResult.location.displayName; 487 hidePVStatus(); 488 } catch (error) { 489 pvFailureReason = error.message || "Unknown PVWatts error."; 490 console.warn("PVWatts fallback used:", error); 491 showPVStatus("We could not complete the location-specific solar lookup, so this estimate is using Riverside's regional production assumption.", true); 492 monthlyProduction = [ 493 annualProduction * 0.045, 494 annualProduction * 0.060, 495 annualProduction * 0.085, 496 annualProduction * 0.105, 497 annualProduction * 0.120, 498 annualProduction * 0.125, 499 annualProduction * 0.125, 500 annualProduction * 0.110, 501 annualProduction * 0.085, 502 annualProduction * 0.065, 503 annualProduction * 0.040, 504 annualProduction * 0.035 505 ]; 506 } 507 } 508 509 const annualSavings = annualProduction * hydroRate; 510 511 let lifetimeHydroSavings = 0; 512 513 for (let year = 0; year < assumptions.lifetimeYears; year++) { 514 const inflatedHydroRate = hydroRate * Math.pow(1 + assumptions.hydroInflationRate, year); 515 lifetimeHydroSavings += annualProduction * inflatedHydroRate; 516 } 517 518 const lifetimeSavings = lifetimeHydroSavings - netCost; 519 520 const monthlyPayment = financingRate === 0 ? 0 : monthlyLoanPayment(netCost, financingRate, assumptions.financingYears); 521 const monthlySolarSavings = annualSavings / 12; 522 523 524 const grossIncomeRequired = annualHydroSpend / (1 - taxRate); 525 526 document.getElementById("systemSize").innerText = suggestedSize; 527 document.getElementById("systemCost").innerText = formatMoney(systemCost); 528 document.getElementById("rebate").innerText = formatMoney(rebate); 529 document.getElementById("netCost").innerText = formatMoney(netCost); 530 document.getElementById("annualProduction").innerText = Math.round(annualProduction).toLocaleString(); 531 document.getElementById("annualSavings").innerText = formatMoney(annualSavings); 532 533 updateLocationEstimateCard(pvWattsUsed, pvWattsLocation, productionPerKwUsed, pvFailureReason); 534 renderMonthlyProductionChart(monthlyProduction); 535 document.getElementById("payback").innerText = formatMoney(lifetimeSavings); 536 document.getElementById("monthlyPayment").innerText = formatMoney(monthlyPayment); 537 document.getElementById("paymentLabel").innerText = financingRate === 0 ? "Estimated Monthly Payment" : "Estimated " + financingLabel + " Payment"; 538 539 540 541 document.getElementById("taxStatement").innerText = 542 "Your current hydro bill of $" + formatMoney(annualHydroSpend) + 543 "/year requires about $" + formatMoney(grossIncomeRequired) + 544 " of pre-tax household income to pay. Based on a typical household tax rate assumption."; 545 546 latestEstimate = { 547 suggestedSystemSizeKw: suggestedSize, 548 estimatedSystemCost: Math.round(systemCost), 549 estimatedRebate: Math.round(rebate), 550 costAfterRebate: Math.round(netCost), 551 annualProductionKwh: Math.round(annualProduction), 552 annualSavings: Math.round(annualSavings), 553 lifetimeSavings: Math.round(lifetimeSavings), 554 monthlyPayment: Math.round(monthlyPayment), 555 hydroRateUsed: hydroRate, 556 productionPerInstalledKw: Math.round(productionPerKwUsed), 557 pvWattsUsed: pvWattsUsed, 558 pvWattsLocation: pvWattsLocation || "", 559 customerClass: propertyType, 560 financingLabel: financingLabel 561 }; 562 563 trackCalculatorUsage(); 564 565 const resultsSection = document.getElementById("solarResults"); 566 const calculateButton = document.getElementById("calculateButton"); 567 const featuredCard = document.querySelector(".metric-featured"); 568 569 calculateButton.innerText = "Calculating Local Solar Production..."; 570 571 setTimeout(function() { 572 resultsSection.style.display = "block"; 573 resultsSection.classList.remove("results-visible"); 574 void resultsSection.offsetWidth; 575 resultsSection.classList.add("results-visible"); 576 577 resultsSection.scrollIntoView({ 578 behavior: "smooth", 579 block: "start" 580 }); 581 582 if (featuredCard) { 583 featuredCard.classList.remove("flash-highlight"); 584 void featuredCard.offsetWidth; 585 featuredCard.classList.add("flash-highlight"); 586 } 587 588 calculateButton.innerText = "Recalculate My Solar Estimate"; 589 sendCalculatorHeight(); 590 }, 250); 591 } 592 593 function copyLeadSummary() { 594 const address = document.getElementById("leadAddress")?.value || document.getElementById("address").value || "Not provided"; 595 const name = document.getElementById("leadName")?.value || "Not provided"; 596 const email = document.getElementById("leadEmail")?.value || "Not provided"; 597 const phone = document.getElementById("leadPhone")?.value || "Not provided"; 598 const monthlyBill = document.getElementById("monthlyBill").value || "Not provided"; 599 600 const summary = 601 "Solar Calculator Lead Summary\n" + 602 "Name: " + name + "\n" + 603 "Email: " + email + "\n" + 604 "Phone: " + phone + "\n" + 605 "Address/City: " + address + "\n" + 606 "Monthly Hydro Bill: $" + monthlyBill + "\n" + 607 "Suggested System Size: " + document.getElementById("systemSize").innerText + " kW\n" + 608 609 "Estimated System Cost: $" + document.getElementById("systemCost").innerText + "\n" + 610 "Estimated Rebate: $" + document.getElementById("rebate").innerText + "\n" + 611 "Cost After Rebate: $" + document.getElementById("netCost").innerText + "\n" + 612 "Estimated Annual Savings: $" + document.getElementById("annualSavings").innerText + "\n" + 613 "Estimated 25-Year Lifetime Savings: $" + document.getElementById("payback").innerText; 614 615 navigator.clipboard.writeText(summary).then(function() { 616 alert("Lead summary copied."); 617 }).catch(function() { 618 alert(summary); 619 }); 620 } 621 updateFinancingOptions();
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.