// contact-form.jsx — Facility Review Request form
// Style departure from homepage forms: feels like a quiet engineering intake
// rather than a sales pop. White card, mono labels, restrained focus state.
const FREE_DOMAINS = new Set([
'gmail.com', 'googlemail.com', 'yahoo.com', 'yahoo.ca', 'yahoo.co.uk',
'outlook.com', 'hotmail.com', 'hotmail.ca', 'live.com', 'msn.com',
'icloud.com', 'me.com', 'mac.com', 'protonmail.com', 'proton.me',
'aol.com', 'mail.com', 'ymail.com', 'rogers.com', 'bell.net', 'shaw.ca',
'sympatico.ca', 'telus.net']);
const isBusinessEmail = (email) => {
if (!email || !email.includes('@')) return false;
const domain = email.split('@')[1]?.toLowerCase();
return domain && !FREE_DOMAINS.has(domain);
};
const isValidPhone = (phone) => {
if (!phone) return false;
const digits = phone.replace(/\D/g, '');
// North American: 10 digits, or 11 with leading 1
return digits.length === 10 || digits.length === 11 && digits.startsWith('1');
};
// ---------- Field primitives ----------
const FieldLabel = ({ children, hint, htmlFor }) =>
;
const fieldBase = {
fontFamily: 'var(--ff-sans)', fontSize: 14.5, color: 'var(--gi-deep-ink)',
padding: '14px 0', border: 'none',
borderBottom: '1.5px solid rgba(15,33,51,0.14)',
background: 'transparent', width: '100%', outline: 'none',
transition: 'border-color 200ms cubic-bezier(.2,0,0,1)',
appearance: 'none', WebkitAppearance: 'none'
};
const TextInput = React.forwardRef(({ error, onChange, ...props }, ref) => {
const [focused, setFocused] = React.useState(false);
return (
setFocused(true)}
onBlur={() => setFocused(false)}
onChange={onChange}
style={{
...fieldBase,
borderBottomColor: error ? '#B33A3A' : focused ? 'var(--gi-soft-green)' : 'rgba(15,33,51,0.14)'
}}
{...props} />);
});
const Select = ({ id, name, value, onChange, error, children, required, placeholder }) => {
const [focused, setFocused] = React.useState(false);
return (
);
};
const Textarea = ({ id, name, value, onChange, placeholder, maxLength }) => {
const [focused, setFocused] = React.useState(false);
return (
);
};
// ---------- Goals checkbox stack ----------
const GoalCheckbox = ({ value, label, checked, onChange }) =>
;
// ---------- Main form ----------
const FacilityReviewForm = () => {
const [submitted, setSubmitted] = React.useState(false);
const [values, setValues] = React.useState({
firstName: '', lastName: '', industry: '', province: '', spend: '',
email: '', phone: '', context: ''
});
const [goals, setGoals] = React.useState([]);
const [errors, setErrors] = React.useState({});
const [sending, setSending] = React.useState(false);
const [submitError, setSubmitError] = React.useState(null);
const update = (k) => (e) => {
setValues((v) => ({ ...v, [k]: e.target.value }));
if (errors[k]) setErrors((er) => ({ ...er, [k]: null }));
};
const toggleGoal = (g) => {
setGoals((arr) => arr.includes(g) ? arr.filter((x) => x !== g) : [...arr, g]);
};
const onSubmit = (e) => {
e.preventDefault();
if (sending) return;
const next = {};
if (!values.firstName.trim()) next.firstName = 'First name required';
if (!values.lastName.trim()) next.lastName = 'Last name required';
if (!values.industry) next.industry = 'Please select your industry';
if (!values.province) next.province = 'Please select your province';
if (!values.spend) next.spend = 'Please select your spend range';
if (!values.email) next.email = 'Business email required';else
if (!isBusinessEmail(values.email)) next.email = 'Please use your business email address';
if (!values.phone) next.phone = 'Phone number required';else
if (!isValidPhone(values.phone)) next.phone = 'Please enter a valid phone number';
setErrors(next);
if (Object.keys(next).length > 0) return;
// GI Link Audit (Part 3, Key Event #1): generate_lead.
// Fires once on successful client-side validation — a leading indicator
// (form-completion intent), independent of the Zoho round-trip below.
if (window.GIGtm && typeof window.GIGtm.generateLead === 'function') {
window.GIGtm.generateLead({
form_id: 'facility-review-request',
form_location: 'contact-page',
industry: values.industry || undefined,
province: values.province || undefined,
spend_range: values.spend || undefined,
goals: goals.length ? goals.join('|') : undefined
});
}
// Map our internal slugs to the EXACT Zoho dropdown option values — Zoho
// rejects any value not in its option list. Kept local (not top-level) so
// they can't collide in the shared babel global scope.
const INDUSTRY_TO_ZOHO = {
'manufacturing': 'Manufacturing',
'food-beverage': 'Food & Beverage',
'warehousing': 'Warehousing & Logistics',
'solar-maintenance': 'Solar maintenance',
'energy-audit': 'Energy Audit',
'agriculture': 'Agriculture',
'commercial-real-estate': 'Commercial Real Estate',
'municipal': 'Municipal / Institutional',
'other': 'Other C&I'
};
// NOTE: "Nva Scotia" is a typo in the Zoho form's own option list. It must
// be sent verbatim or the value won't match. Fix it in Zoho, then update here.
const PROVINCE_TO_ZOHO = {
'ontario': 'Ontario',
'alberta': 'Alberta',
'nova-scotia': 'Nva Scotia',
'bc': 'British Columbia',
'saskatchewan': 'Saskatchewan',
'quebec': 'Quebec',
'manitoba': 'Manitoba',
'new-brunswick': 'New Brunswick',
'newfoundland-and-labrador': 'Newfoundland and Labrador',
'prince-edward-island': 'Prince Edward Island'
};
const SPEND_TO_ZOHO = {
'under-5k': '< $5k / month',
'5k-10k': '$5 - 10k / month',
'10k-25k': '$10 - 25k / month',
'25k-75k': '$25 - 75k / month',
'75k-plus': '$75k+ / month'
};
const GOAL_TO_ZOHO = {
'cost-reduction': 'Reduce electricity costs',
'solar-feasibility': 'Solar or storage feasibility',
'compliance': 'Compliance readiness (BEPS / ESG)',
'full-review': 'Full energy review',
'incentives': 'Incentive eligibility',
'storage': 'Storage & resilience'
};
// Zoho Forms submit endpoint — from the form's "Source Code" embed. NOTE:
// this formperma differs from the public/iframe link; /htmlRecords/submit is
// the URL that actually records entries.
const ZOHO_ACTION = 'https://forms.zohopublic.com/greenintegrations/form/StartYourAssesment/formperma/vsvhoAZGo1FNMLpAbsBA-N3hCFNSwI5F7K7Dab_gmpg/htmlRecords/submit';
// Field names are taken verbatim from the Zoho embed — renaming or dropping
// them silently empties the value on submission.
const fd = new FormData();
fd.append('zf_referrer_name', '');
fd.append('zf_redirect_url', '');
fd.append('zc_gad', '');
fd.append('Name_First', values.firstName.trim());
fd.append('Name_Last', values.lastName.trim());
fd.append('Dropdown1', INDUSTRY_TO_ZOHO[values.industry] || values.industry);
fd.append('Dropdown2', PROVINCE_TO_ZOHO[values.province] || values.province);
fd.append('Dropdown3', SPEND_TO_ZOHO[values.spend] || values.spend);
// MultipleChoice is a multi-select — append once per chosen goal, mirroring
// a native