Hello, World!
import React, { useMemo, useState } from "react"; // Planet Electrical — Solar ROI Calculator (Embed) // Single-file React component. Production-ready, clean Tailwind UI. // Assumptions are NZ‑centric and editable in the "Defaults" section. export default function SolarRoiCalculator() { // ========= Defaults (you can tweak these) ========= const DEFAULTS = { pricePerKWh: 0.35, // $/kWh retail feedInTariff: 0.08, // $/kWh export credit baselineYield: 1350, // kWh/kW/year (Auckland-ish, north @ ~30° tilt) sysCostPerKW: 2800, // NZD/kW installed (PV only) batteryInstalledCost: 16000, // NZD (e.g., Tesla Powerwall as placeholder) evChargerCost: 2000, // NZD for smart EVSE install baseline } as const; // ========= State ========= const [dailyUse, setDailyUse] = useState(22); // kWh/day const [daytimeShare, setDaytimeShare] = useState(55); // % of use that happens 8am–6pm const [systemSize, setSystemSize] = useState(7.2); // kW DC const [panelWatt, setPanelWatt] = useState(440); // for roof fit calc only const [roofArea, setRoofArea] = useState(70); // m² usable area const [orientation, setOrientation] = useState("N"); // N, NE/NW, E/W, SE/SW, S const [tilt, setTilt] = useState(25); // degrees const [shading, setShading] = useState(5); // % loss const [pricePerKWh, setPricePerKWh] = useState(DEFAULTS.pricePerKWh); const [feedIn, setFeedIn] = useState(DEFAULTS.feedInTariff); const [sysCostPerKW, setSysCostPerKW] = useState(DEFAULTS.sysCostPerKW); const [hasBattery, setHasBattery] = useState(false); const [batteryKWh, setBatteryKWh] = useState(13.5); const [batteryCost, setBatteryCost] = useState(DEFAULTS.batteryInstalledCost); const [hasEv, setHasEv] = useState(false); const [evChargerCost, setEvChargerCost] = useState(DEFAULTS.evChargerCost); // ========= Helpers ========= const fmtNZD = (n: number) => n.toLocaleString("en-NZ", { style: "currency", currency: "NZD", maximumFractionDigits: 0 }); const fmtKWh = (n: number) => `${n.toLocaleString("en-NZ", { maximumFractionDigits: 0 })} kWh`; const clamp = (v: number, a: number, b: number) => Math.max(a, Math.min(b, v)); // Orientation multipliers (simple heuristic) const orientationFactor = useMemo(() => { switch (orientation) { case "N": return 1.0; case "NE/NW": return 0.95; case "E/W": return 0.9; case "SE/SW": return 0.85; case "S": return 0.75; default: return 1.0; } }, [orientation]); // Tilt factor — gentle parabolic penalty away from ~30° const tiltFactor = useMemo(() => { const d = Math.abs(tilt - 30); // ~1% loss per 5° away from 30°, capped const penalty = Math.min(0.12, (d / 5) * 0.01); return 1 - penalty; }, [tilt]); const shadingFactor = useMemo(() => 1 - clamp(shading, 0, 60) / 100, [shading]); // Roof fit estimate (kW) from area + panel W const roofFitKW = useMemo(() => { const panelAreaM2 = 1.94; // typical 72/108 cell module footprint const panelsFit = Math.floor(roofArea / panelAreaM2); const kW = (panelsFit * panelWatt) / 1000; return Math.max(0, kW); }, [roofArea, panelWatt]); const suggestedSizeKW = useMemo(() => { // Rule of thumb: aim to cover ~90–110% of annual usage if feed-in is low, // but not exceed roof/budget limits. Provide a nudge only. const annualUse = dailyUse * 365; const yieldPerKW = DEFAULTS.baselineYield * orientationFactor * tiltFactor * shadingFactor; const sizeForUsage = annualUse / yieldPerKW; // kW to match usage // Budget limit const budgetCapKW = Infinity; // You could add a budget input and cap by budget/sysCostPerKW. return clamp(Number((Math.min(sizeForUsage * 0.95, roofFitKW, budgetCapKW)).toFixed(2)), 0, 50); }, [dailyUse, roofFitKW, orientationFactor, tiltFactor, shadingFactor]); // ========= Core calcs ========= const results = useMemo(() => { const annualUse = dailyUse * 365; // kWh const yieldPerKW = DEFAULTS.baselineYield * orientationFactor * tiltFactor * shadingFactor; const annualGen = systemSize * yieldPerKW; // kWh/yr // Self-consumption model (simple heuristic): // Start with daytime share of annual use as a cap for direct consumption const dayFrac = clamp(daytimeShare / 100, 0, 1); const directUseCap = annualUse * dayFrac; // Direct self-consumption (bounded by generation) const directSelf = Math.min(annualGen, directUseCap); // Battery & EV improve self-consumption by shifting some of the exported energy const exportedBeforeShift = Math.max(0, annualGen - directSelf); let extraFrac = 0; if (hasBattery) { // Battery can increase self-consumption up to +25pp depending on surplus // and size; rough cap scaled by battery size vs system size. const battScale = clamp(batteryKWh / (systemSize * 2), 0, 1); // 0–1 heuristic extraFrac += 0.25 * battScale; } if (hasEv) { // Smart EV charging behaviour can add ~5pp extraFrac += 0.05; } const targetSelfFrac = clamp(dayFrac + extraFrac, 0, 0.98); const targetSelf = Math.min(annualGen, annualUse * targetSelfFrac); // Additional shifted self-consumption is limited by exported surplus const shifted = Math.min(exportedBeforeShift, Math.max(0, targetSelf - directSelf)); const selfConsumed = directSelf + shifted; const exported = Math.max(0, annualGen - selfConsumed); // $ figures const billBefore = annualUse * pricePerKWh; const valueSelf = selfConsumed * pricePerKWh; const valueExport = exported * feedIn; const annualSavings = valueSelf + valueExport; const pvCost = systemSize * sysCostPerKW; const capex = pvCost + (hasBattery ? batteryCost : 0) + (hasEv ? evChargerCost : 0); const billAfter = Math.max(0, billBefore - annualSavings); const paybackYears = annualSavings > 0 ? capex / annualSavings : Infinity; // ROI (simple) — year 1 savings / capex const roiPct = annualSavings > 0 && capex > 0 ? (annualSavings / capex) * 100 : 0; return { annualUse, annualGen, yieldPerKW, selfConsumed, exported, billBefore, billAfter, annualSavings, capex, paybackYears, roiPct, directSelf, shifted, targetSelfFrac, }; }, [ dailyUse, daytimeShare, systemSize, orientationFactor, tiltFactor, shadingFactor, pricePerKWh, feedIn, sysCostPerKW, hasBattery, batteryKWh, batteryCost, hasEv, evChargerCost, ]); const copyEmbed = async () => { const html = `\n\n`; try { await navigator.clipboard.writeText(html); alert("Embed code copied. Paste into a Squarespace code block."); } catch (e) { alert("Couldn't copy — select & copy manually:\"); console.log(html); } }; return (Solar savings & ROI estimator
Quick, NZ‑centric guidance. Edit inputs to match your home — outputs are estimates only.
{/* Controls */}
Household & usage
Roof & system
Options
{hasBattery && (
)}
{hasEv && (
)}
{/* Results */}
Notes & assumptions
Roof can fit about {roofFitKW.toFixed(1)} kW of panels.
- Energy yield uses a baseline of {DEFAULTS.baselineYield} kWh/kW/yr, adjusted for orientation, tilt, and shading. Real‑world results vary.
- Self‑consumption depends on when you use power. Adjust "Daytime usage" and options for battery/EV to see the effect.
- Costs are ballpark and exclude switchboard or roof remediation. We’ll confirm after a site visit.
- Export credits vary by retailer and plan. Enter your plan’s rate for accuracy.
- This is guidance only, not a quote. For a detailed design, book a free assessment with Planet Electrical.
{children}
;
}
function CardTitle({ children }: { children: React.ReactNode }) {
return {children}
; } function Stat({ label, value, hint }: { label: string; value: string; hint?: string }) { return ({label}
{value}
{hint && {hint}
}
onChange(Number(e.target.value))} className="w-full" />
onChange(Number(e.target.value))} className="w-28 input" />
{suffix && {suffix}}
{!suffix && prefix && {prefix}}