PageSourceSearch

https://riverside-solar-estimator.pages.dev/app.js

js riverside-solar-estimator.pages.dev collected 2026-10-03 05:53:44 UTC 22,766 bytes, 621 lines download raw bytes

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.