// solar-calc-calculator.jsx
// The 4-step calculator card. Bright white card on cream.
// Sage green for progress + UI. Amber appears only on the incentive band.
//
// Step flow:
// 0 — Electricity Basics (province, monthly bill)
// 1 — Facility Profile (industry, size, roof type)
// 2 — Teaser + Lead Gate (visible savings range, blurred metrics, form)
// 3 — Calculating animation → Results
//
// State held by parent component .
// ----- Reusable bits -----------------------------------------
const ProgressRail = ({ step, labels }) => (
{labels.map((label, i) => {
const active = i <= step;
const current = i === step;
return (
);
})}
);
const FieldLabel = ({ children, hint }) => (
{children}
{hint && (
{hint}
)}
);
const inputStyle = (focus) => ({
width: '100%', padding: '14px 16px',
background: '#fff',
border: `1px solid ${focus ? SC_SAGE : 'rgba(15,33,51,0.16)'}`,
borderRadius: 2,
fontFamily: 'var(--ff-sans)', fontSize: 15,
color: 'var(--gi-deep-ink)',
outline: 'none',
transition: 'border-color 180ms cubic-bezier(.2,0,0,1)',
boxShadow: focus ? `0 0 0 3px ${SC_SAGE_SOFT}` : 'none'
});
const TextInput = ({ value, onChange, placeholder, type = 'text', ...rest }) => {
const [focus, setFocus] = React.useState(false);
return (
onChange(e.target.value)}
placeholder={placeholder}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
style={inputStyle(focus)}
{...rest}
/>
);
};
const Select = ({ value, onChange, options, placeholder }) => {
const [focus, setFocus] = React.useState(false);
return (
onChange(e.target.value)}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
style={{
...inputStyle(focus),
appearance: 'none', paddingRight: 44, cursor: 'pointer',
color: value ? 'var(--gi-deep-ink)' : 'var(--gi-graphite)'
}}>
{placeholder}
{options.map(o => (
{o.label || o}
))}
);
};
// ----- Step 1: Electricity Basics ----------------------------
const Step1 = ({ data, setData, onNext, error }) => {
return (
Your facility’s electricity costs
Two numbers get us started — your province sets local utility rates and
irradiance; your bill anchors the whole estimate.
Province
setData({ ...data, province: v })}
placeholder="Select your province"
options={PROVINCES.map(p => ({ value: p.code, label: p.name + ' (' + p.code + ')' }))}
/>
{error && (
{error}
)}
);
};
// ----- Step 2: Facility Profile -------------------------------
const Step2 = ({ data, setData, onNext, onBack, error }) => {
return (
Tell us about your facility
Industry, size, and roof type help us frame the system — and route your
estimate to the right specialist for first contact.
{/* Industry tiles */}
Industry
{INDUSTRIES.map(ind => {
const active = data.industry === ind.id;
return (
setData({ ...data, industry: ind.id })}
style={{
background: active ? SC_SAGE_TINT : '#fff',
border: `1px solid ${active ? SC_SAGE : 'rgba(15,33,51,0.16)'}`,
borderRadius: 2,
padding: '18px 14px',
cursor: 'pointer',
textAlign: 'left',
display: 'flex', alignItems: 'center', gap: 12,
transition: 'background 180ms cubic-bezier(.2,0,0,1), border-color 180ms cubic-bezier(.2,0,0,1)',
boxShadow: active ? `0 0 0 3px ${SC_SAGE_SOFT}` : 'none'
}}>
{ind.label}
);
})}
Facility size
setData({ ...data, size: v })}
placeholder="Select size"
options={FACILITY_SIZES}
/>
Roof type
setData({ ...data, roof: v })}
placeholder="Select roof type"
options={ROOF_TYPES}
/>
{error && (
{error}
)}
);
};
// ----- Step 3: Teaser + Lead Gate ----------------------------
const Step3 = ({ data, setData, estimates, onSubmit, onBack, error }) => {
return (
Your estimate is ready
Here’s your savings range — unlock the full estimate.
{/* Teaser cards: savings visible, two others blurred */}
{/* Lead gate explainer */}
Unlock your full estimate — including project cost range and incentive breakdown.
Your address lets us pull satellite roof data and precise local
irradiance — so the number you get is specific to your building.
{/* Lead form — lean: business name + address required, plus reach-out */}
Business name
setData({ ...data, businessName: v })}
placeholder="e.g. Acme Manufacturing Inc."
/>
Contact name
setData({ ...data, contactName: v })}
placeholder="Optional"
/>
Facility address
setData({ ...data, address: v })}
placeholder="Street, city, province, postal code"
/>
Work email
setData({ ...data, email: v })}
placeholder="name@company.ca"
/>
Phone
setData({ ...data, phone: v })}
placeholder="(000) 000-0000"
/>
Your information is used only to generate and deliver your site-specific
estimate. No spam. No third-party sharing.
{error && (
{error}
)}
);
};
const TeaserCard = ({ label, value, blurred = false, highlight = false }) => (
{highlight && (
)}
{label}
{blurred ? (
) : (
{value}
)}
);
// ----- Step 4: Calculating + Results -------------------------
const Calculating = () => {
const lines = [
'Pulling provincial irradiance',
'Sizing system to annual profile',
'Applying incentive programs'
];
return (
Analysing your facility data…
{lines.map((l, i) => (
· {l}
))}
);
};
const Results = ({ estimates, data, onReset }) => {
const province = estimates.province;
return (
Site-specific estimate · {province.name}
Your estimated savings — full picture.
Directional ranges for {data.businessName || 'your facility'}. A
specialist will review these numbers with you within one business day.
{/* 4 result cards */}
{/* Incentive band — this is the ONE amber moment on the page */}
Up to {fmt$k(estimates.incentive.high)} in incentives
Applicable in {province.name}: {province.programs} . CCA Class 43.2 accelerated depreciation also applies.
{/* Next step CTA */}
A Green Integrations specialist will contact you within 1 business day
to review this estimate and discuss a site-specific analysis.
Start a new estimate
{/* Disclaimer */}
This estimate is directional only. Actual savings depend on your
consumption profile, roof conditions, shading, utility rate structure,
and project design. A site-specific analysis is required to produce a
bankable projection. All figures in CAD.
);
};
const ResultCard = ({ label, value, sub, highlight = false }) => (
{highlight && (
)}
{label}
{value}
{sub}
);
// ----- Nav row -------------------------------------------------
const NavRow = ({ onBack, onNext, nextLabel }) => (
{onBack ? (
Back
) :
}
e.currentTarget.style.background = '#2E4A39'}
onMouseOut={(e) => e.currentTarget.style.background = 'var(--gi-soft-green)'}>
{nextLabel}
);
// ----- Top-level: ScCalculator section ------------------------
const ScCalculator = React.forwardRef((props, ref) => {
const [step, setStep] = React.useState(0);
const [error, setError] = React.useState('');
const [calculating, setCalculating] = React.useState(false);
const [done, setDone] = React.useState(false);
const [data, setData] = React.useState({
province: '', monthlyBill: '',
industry: '', size: '', roof: '',
businessName: '', contactName: '', address: '', email: '', phone: ''
});
const estimates = React.useMemo(() => calculateEstimates({
provinceCode: data.province,
monthlyBill: Number(data.monthlyBill) || 0
}), [data.province, data.monthlyBill]);
const handleStep1 = () => {
setError('');
if (!data.province) return setError('Please select your province.');
if (!data.monthlyBill) return setError('Please enter your average monthly electricity bill.');
if (Number(data.monthlyBill) < 500) return setError('This calculator is built for commercial facilities — monthly bills typically start at $500. For residential, please consult a residential installer.');
setStep(1);
};
const handleStep2 = () => {
setError('');
if (!data.industry) return setError('Please choose an industry.');
if (!data.size) return setError('Please select a facility size.');
if (!data.roof) return setError('Please select a roof type.');
setStep(2);
};
const handleSubmit = () => {
setError('');
if (!data.businessName.trim()) return setError('Business name is required.');
if (!data.address.trim()) return setError('Facility address is required — it’s how we generate a building-specific estimate.');
if (!data.email.trim() || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(data.email)) return setError('Please enter a valid work email.');
if (!data.phone.trim()) return setError('Please enter a phone number.');
setCalculating(true);
setStep(3);
setTimeout(() => {
setCalculating(false);
setDone(true);
// GI Link Audit (Part 3, Key Event #3): solar_calc_complete.
// Fires when the simulated calculation finishes and the user lands on
// the results panel. Also doubles as a soft lead signal — by this point
// the user has submitted business name, email, and phone in Step 3.
if (window.GIGtm && typeof window.GIGtm.solarCalcComplete === 'function') {
window.GIGtm.solarCalcComplete({
location: 'res-solar-calculator',
province: data.province || undefined,
industry: data.industry || undefined,
facility_size: data.size || undefined,
roof_type: data.roof || undefined,
monthly_bill: Number(data.monthlyBill) || undefined,
estimated_savings: estimates && estimates.annualSavings || undefined,
estimated_system_kw: estimates && estimates.systemKw || undefined
});
// The form on Step 3 collects business email + phone, which the
// audit doc also classifies as a `generate_lead` moment.
if (typeof window.GIGtm.generateLead === 'function') {
window.GIGtm.generateLead({
form_id: 'solar-calculator',
form_location: 'res-solar-calculator',
industry: data.industry || undefined,
province: data.province || undefined
});
}
}
}, 2200);
};
const handleReset = () => {
setStep(0); setError(''); setCalculating(false); setDone(false);
setData({ province: '', monthlyBill: '', industry: '', size: '', roof: '',
businessName: '', contactName: '', address: '', email: '', phone: '' });
if (ref && ref.current) ref.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
return (
{/* Section running header */}
Estimate
Step {Math.min(step + 1, 4)} of 04
{/* Card */}
{step === 0 && }
{step === 1 && setStep(0)} error={error} />}
{step === 2 && estimates && setStep(1)} error={error} />}
{step === 3 && calculating && }
{step === 3 && done && estimates && }
);
});
Object.assign(window, { ScCalculator });