// gi-zoho-forms.jsx — shared, parameterized Zoho form engine // ===================================================================== // ONE submit engine + thin presentation wrappers (modal | inline) for every // Zoho form on the site. Design ported from form-popup.html. // // ARCHITECTURE (engine vs presentation — kept deliberately separate) // • useGiZohoForm(form) — THE ENGINE. State + validation + native // hidden-iframe POST + zf_referrer_name + success/error. No markup. // • GiZohoModal — modal presentation (overlay, focus-trap, ESC, // click-outside, scroll-lock, aria) wrapping the engine + generic fields. // • GiZohoNewsletterForm — inline presentation (no modal), bespoke dark-band // markup, same engine. Exposed on window for blog-page.jsx's SubscribeBand. // // ADD A FORM = one entry in GI_ZOHO_FORMS (triggerHref, htmlRecords action, // copy, fields[] of SEMANTIC types). No per-button edits, no new files. // // FIELD SCHEMA (semantic → verbatim Zoho names; renaming a name empties the // record per Zoho's own warning): // name → Name_First + Name_Last (First / Last) // email → Email // phone → PhoneNumber_countrycode (compname PhoneNumber, fieldType 11) // message → MultiLine (textarea) // address → Address_AddressLine1/2, _City, _Region, _ZipCode, _Country(select) // — Country uses the ONE shared GI_ZF_COUNTRIES constant. // Each field has `required`. Each entry also: presentation, action, heading, // intro (lifted verbatim from the Zoho export), submit/success copy. // // TRIGGERING: one delegated document click matches against a form's // triggerHref (or [data-gi-form]) and opens the modal — ZERO button edits; // the href stays so JS-off / ⌘-click still reach the hosted form. The // newsletter is inline (rendered in the page), not delegate-triggered. // // SUBMIT: native multipart
; the iframe `load` event // is the success signal (15s timeout + offline check = error). zf_redirect_url // stays blank so Zoho renders its thank-you into the hidden iframe. // // Wrapped in an IIFE (shared babel global scope). Exposes window.giZohoForms + // window.GiZohoNewsletterForm. Self-mount + style inject are idempotent. (function () { if (window.__giZohoFormsBooted) return; window.__giZohoFormsBooted = true; // =================================================================== // Shared country list (full Zoho option set; verbatim from the // Book a system inspection export). Used by every `address` field. // =================================================================== const GI_ZF_COUNTRIES = ["Afghanistan","Akrotiri","Albania","Algeria","American Samoa","Andorra","Angola","Anguilla","Antarctica","Antigua and Barbuda","Argentina","Armenia","Aruba","Ashmore and Cartier Islands","Australia","Austria","Azerbaijan","Bahrain","Bangladesh","Barbados","Bassas Da India","Belarus","Belgium","Belize","Benin","Bermuda","Bhutan","Bolivia","Bosnia and Herzegovina","Botswana","Bouvet Island","Brazil","British Indian Ocean Territory","British Virgin Islands","Brunei","Bulgaria","Burkina Faso","Burma","Burundi","Cambodia","Cameroon","Canada","Cape Verde","Caribbean Netherlands","Cayman Islands","Central African Republic","Chad","Chile","China","Christmas Island","Clipperton Island","Cocos (Keeling) Islands","Colombia","Comoros","Cook Islands","Coral Sea Islands","Costa Rica","Cote D'Ivoire","Croatia","Cuba","Curaçao","Cyprus","Czech Republic","Democratic Republic of the Congo","Denmark","Dhekelia","Djibouti","Dominica","Dominican Republic","Ecuador","Egypt","El Salvador","Equatorial Guinea","Eritrea","Estonia","Ethiopia","Europa Island","Falkland Islands (Islas Malvinas)","Faroe Islands","Federated States of Micronesia","Fiji","Finland","France","French Guiana","French Polynesia","French Southern and Antarctic Lands","Gabon","Gaza Strip","Georgia","Germany","Ghana","Gibraltar","Glorioso Islands","Greece","Greenland","Grenada","Guadeloupe","Guam","Guatemala","Guernsey","Guinea","Guinea-bissau","Guyana","Haiti","Heard Island and Mcdonald Islands","Holy See (Vatican City)","Honduras","Hong Kong","Hungary","Iceland","India","Indonesia","Iran","Iraq","Ireland","Isle of Man","Israel","Italy","Jamaica","Jan Mayen","Japan","Jersey","Jordan","Juan De Nova Island","Kazakhstan","Kenya","Kiribati","Kosovo","Kuwait","Kyrgyzstan","Laos","Latvia","Lebanon","Lesotho","Liberia","Libya","Liechtenstein","Lithuania","Luxembourg","Macau","Macedonia","Madagascar","Malawi","Malaysia","Maldives","Mali","Malta","Marshall Islands","Martinique","Mauritania","Mauritius","Mayotte","Mexico","Moldova","Monaco","Mongolia","Montenegro","Montserrat","Morocco","Mozambique","Myanmar","Namibia","Nauru","Navassa Island","Nepal","Netherlands Antilles","Netherlands","New Caledonia","New Zealand","Nicaragua","Niger","Nigeria","Niue","Norfolk Island","North Korea","Northern Mariana Islands","Norway","Oman","Pakistan","Palau","Palestine","Panama","Papua New Guinea","Paracel Islands","Paraguay","Peru","Philippines","Pitcairn Islands","Poland","Portugal","Puerto Rico","Qatar","Republic of the Congo","Reunion","Romania","Russia","Rwanda","Saint Barthélemy","Saint Helena","Saint Kitts and Nevis","Saint Lucia","Saint Martin","Saint Pierre and Miquelon","Saint Vincent and the Grenadines","Samoa","San Marino","Sao Tome and Principe","Saudi Arabia","Senegal","Serbia","Seychelles","Sierra Leone","Singapore","Sint Maarten","Slovakia","Slovenia","Solomon Islands","Somalia","South Africa","South Georgia and the South Sandwich Islands","South Korea","South Sudan","Spain","Spratly Islands","Sri Lanka","Sudan","Suriname","Svalbard","Swaziland","Sweden","Switzerland","Syria","Taiwan","Tajikistan","Tanzania","Thailand","The Bahamas","The Gambia","Timor-leste","Togo","Tokelau","Tonga","Trinidad and Tobago","Tromelin Island","Tunisia","Turkey","Turkmenistan","Turks and Caicos Islands","Tuvalu","Uganda","Ukraine","United Arab Emirates","United Kingdom","United States","Uruguay","Uzbekistan","Vanuatu","Venezuela","Vietnam","Virgin Islands","Wake Island","Wallis and Futuna","West Bank","Western Sahara","Yemen","Zambia","Zimbabwe","Åland Islands"]; // =================================================================== // FORM REGISTRY — the only per-form surface // =================================================================== // action URLs are VERBATIM from each Zoho export. Several internal slugs are // intentionally mismatched to the label (Zoho form duplication) — DO NOT // "fix" them; the formperma token is the source of truth. const NAME = { type: 'name', required: true }; const EMAIL = { type: 'email', required: true }; const PHONE = { type: 'phone', required: true }; const MSG = { type: 'message', required: true }; const GI_ZOHO_FORMS = { // 1 — Generic 20-min CTA (33 instances sitewide). [GENERIC stays generic.] 'book-call': { presentation: 'modal', triggerHref: 'https://zfrmz.com/vjBUOoKTlKt4YAOW19oO', action: 'https://forms.zohopublic.com/greenintegrations/form/BookaGovernmentSectorConsultation/formperma/03Y36VrGJLbcA2YJEaKdZB_cHxzG8A-M2lfFtBa9pLQ/htmlRecords/submit', eyebrow: 'Complimentary Consultation', heading: 'Book a 20-Minute Consultation', intro: 'Schedule a complimentary 20-minute call with our team to discuss your facility, energy goals, available incentives, and opportunities to reduce electricity costs and improve long-term energy performance.', fields: [NAME, EMAIL, PHONE, MSG], }, // 2 — Book a system inspection (solar maintenance). Has optional Address. 'system-inspection': { presentation: 'modal', triggerHref: 'https://zfrmz.com/kl0IqSRr6mTi5yhN8CWX', action: 'https://forms.zohopublic.com/greenintegrations/form/Bookasysteminspection/formperma/rlSMU-5yNokjZeql3vQ5QHgIe51BoSsEJgRZ2x0d4NA/htmlRecords/submit', eyebrow: 'Existing System Owners', heading: 'Book a system inspection', intro: 'Schedule a professional inspection to verify system performance, identify potential issues, and help maximize the reliability and long-term value of your solar investment.', submitLabel: 'Request Inspection', successHeading: 'Your inspection request has been received.', successBody: 'Thank you. We will review your system details and respond within one business day.', fields: [NAME, EMAIL, PHONE, { type: 'address', required: false }, MSG], }, // 3 — Book a Partner Call (partners page). 'partner-call': { presentation: 'modal', triggerHref: 'https://zfrmz.com/8SSu2kceVHIK6aBPhv3V', action: 'https://forms.zohopublic.com/greenintegrations/form/BookaPartnerCall/formperma/p_6Yo-gxN2FB8P10ZwqetFzRQwr-lHt7JZXx0pY6P4A/htmlRecords/submit', eyebrow: 'Partnerships', heading: 'Book a Partner Call', intro: 'Schedule a quick call to explore partnership opportunities, discuss collaboration ideas, and learn how we can create value together for commercial and industrial clients.', submitLabel: 'Request Call', successBody: 'Thank you. Our partnerships team will be in touch within one business day.', fields: [NAME, EMAIL, PHONE, MSG], }, // 4 — Become a Partner (partners page). 'become-partner': { presentation: 'modal', triggerHref: 'https://zfrmz.com/Kyz6jnB2SOU0GYGErcdK', action: 'https://forms.zohopublic.com/greenintegrations/form/BecomeaPartner/formperma/1X1X6T-zX1b8kpuglHby627LBf2sGH5c3yzcMblpZVo/htmlRecords/submit', eyebrow: 'Partner Program', heading: 'Become a Partner', intro: 'Join our network of trusted partners and explore opportunities to collaborate on energy projects, share expertise, and create value for clients across Canada.', submitLabel: 'Apply to Partner', successHeading: 'Your application has been received.', successBody: 'Thank you. Our partnerships team will review your details and respond within one business day.', fields: [NAME, EMAIL, PHONE, MSG], }, // 5 — Manufacturing (trigger is a forms.zohopublic VIEW link). [slug ok] 'manufacturing': { presentation: 'modal', triggerHref: 'https://forms.zohopublic.com/greenintegrations/form/WebsiteManufacturing2026/formperma/wowT3kTsYRhwV-gkkHkkYkzsy_6-PByGwIWKcd37rxc', action: 'https://forms.zohopublic.com/greenintegrations/form/WebsiteManufacturing2026/formperma/23eIhqYBtz7CF77p4h7uR-izi6CGph2W-veshIWYB9I/htmlRecords/submit', eyebrow: 'Complimentary Consultation', heading: 'Book a Manufacturing Energy Consultation', intro: 'Book a complimentary manufacturing sector consultation to discuss your facility, electricity costs, available incentives, and opportunities to improve operational efficiency, reduce costs, and support long-term competitiveness.', fields: [NAME, EMAIL, PHONE, MSG], }, // 6 — Warehousing & Logistics. [action slug intentionally "Manufacturing"] 'warehousing': { presentation: 'modal', triggerHref: 'https://zfrmz.com/BnwyQdPWyiVX0XNZ9Uap', action: 'https://forms.zohopublic.com/greenintegrations/form/BookaManufacturingEnergyConsultation/formperma/WOERe_gLcposvPDn-W1cCbSrf5u5TWVfURANrp0-VvE/htmlRecords/submit', eyebrow: 'Complimentary Consultation', heading: 'Book a Warehousing & Logistics Energy Consultation', intro: 'Book a complimentary consultation to discuss your facility, electricity costs, available incentives, and opportunities to improve energy performance, reduce operating expenses, and support long-term operational efficiency.', fields: [NAME, EMAIL, PHONE, MSG], }, // 7 — Food & Beverage. [action slug intentionally "Warehousing"] 'food-beverage': { presentation: 'modal', triggerHref: 'https://zfrmz.com/Ub3c8eUalGfwZwcRscVB', action: 'https://forms.zohopublic.com/greenintegrations/form/BookaWarehousingLogisticsEnergyConsultation/formperma/IXCNcW6gCSSm9n0_NJ92q7Usrt_Y2dUOFy9gJ3IUmK0/htmlRecords/submit', eyebrow: 'Complimentary Consultation', heading: 'Book a Food & Beverage Energy Consultation', intro: 'Book a complimentary consultation to discuss your facility, electricity costs, available incentives, and opportunities to improve energy performance while supporting production efficiency, cost control, and long-term operational resilience.', fields: [NAME, EMAIL, PHONE, MSG], }, // 8 — Agriculture. [action slug intentionally "CIExperts...MalcolmDassin"; // export h2 is an internal label, so we use a clean user-facing heading.] 'agriculture': { presentation: 'modal', triggerHref: 'https://zfrmz.com/YYeFcdEY5IaGNvoCVM0P', action: 'https://forms.zohopublic.com/greenintegrations/form/CIExpertsAgriculturalMalcolmDassin/formperma/Cu8IZ7tSdEuLqwy0eH8GRSISBYSCZ_UKPeNnTLiTjiU/htmlRecords/submit', eyebrow: 'Complimentary Consultation', heading: 'Book an Agriculture Energy Consultation', intro: 'Book a complimentary agriculture sector consultation to discuss your operation, electricity costs, available incentives, and opportunities to improve energy performance while supporting long-term growth and profitability.', fields: [NAME, EMAIL, PHONE, MSG], }, // 9 — Commercial Real Estate. [action slug intentionally "FoodBeverage"] 'cre': { presentation: 'modal', triggerHref: 'https://zfrmz.com/y0HdnM9j70rWzPLTWfsA', action: 'https://forms.zohopublic.com/greenintegrations/form/BookaFoodBeverageEnergyConsultation/formperma/fXbxezbbhZ0iOu4y4OHSAqk38Hq9c2gDYqTH0V72r4w/htmlRecords/submit', eyebrow: 'Complimentary Consultation', heading: 'Book a Commercial Real Estate Consultation', intro: 'Book a complimentary consultation to discuss your property portfolio, electricity costs, available incentives, and opportunities to improve building performance, reduce operating expenses, and support long-term asset value.', fields: [NAME, EMAIL, PHONE, MSG], }, // 10 — Government / Institutional. [action slug intentionally "CommercialRealEstate"] 'government': { presentation: 'modal', triggerHref: 'https://zfrmz.com/DFbsr7h1BGUMoyOYlJid', action: 'https://forms.zohopublic.com/greenintegrations/form/BookaCommercialRealEstateConsultation/formperma/zOVML-6FMKZGnbfQ0Olvk8HSmJiU2fsFxGz4JiJ_250/htmlRecords/submit', eyebrow: 'Complimentary Consultation', heading: 'Book a Government Sector Consultation', intro: 'Book a complimentary consultation to discuss your facility, energy objectives, available funding opportunities, and strategies to improve energy performance, reduce electricity costs, and support long-term sustainability goals.', fields: [NAME, EMAIL, PHONE, MSG], }, // 11 — Newsletter. INLINE (rendered in blog-page SubscribeBand). Name + Email. 'newsletter': { presentation: 'inline', action: 'https://forms.zohopublic.com/greenintegrations/form/Newsletter/formperma/j0a22LrDHKtKtVHSRuDM1O7hXOVYVygtHeRUFM_Wb70/htmlRecords/submit', heading: 'New Analysis. Once a Month.', intro: 'Stay informed with monthly insights on commercial and industrial energy in Canada.', submitLabel: 'Subscribe', fields: [NAME, EMAIL], }, }; // =================================================================== // Field expansion (semantic → concrete Zoho inputs) — single source of // truth shared by init / validation / rendering. // =================================================================== const fieldSpecs = (field) => { switch (field.type) { case 'name': return [ { name: 'Name_First', label: 'First Name', kind: 'text', required: !!field.required, autoComplete: 'given-name', col: 'half' }, { name: 'Name_Last', label: 'Last Name', kind: 'text', required: !!field.required, autoComplete: 'family-name', col: 'half' }, ]; case 'email': return [{ name: 'Email', label: 'Email', kind: 'email', required: !!field.required, autoComplete: 'email', col: 'half' }]; case 'phone': return [{ name: 'PhoneNumber_countrycode', label: 'Phone Number', kind: 'tel', required: !!field.required, autoComplete: 'tel', col: 'half' }]; case 'message': return [{ name: 'MultiLine', label: field.label || 'Leave us a few words', kind: 'textarea', required: !!field.required, col: 'full', maxLength: 65535, placeholder: field.placeholder || '' }]; case 'address': return [ { name: 'Address_AddressLine1', label: 'Street Address', kind: 'text', required: false, autoComplete: 'address-line1', col: 'full' }, { name: 'Address_AddressLine2', label: 'Address Line 2', kind: 'text', required: false, autoComplete: 'address-line2', col: 'full' }, { name: 'Address_City', label: 'City', kind: 'text', required: false, autoComplete: 'address-level2', col: 'half' }, { name: 'Address_Region', label: 'State / Region / Province', kind: 'text', required: false, autoComplete: 'address-level1', col: 'half' }, { name: 'Address_ZipCode', label: 'Postal / Zip Code', kind: 'text', required: false, autoComplete: 'postal-code', col: 'half' }, { name: 'Address_Country', label: 'Country', kind: 'select', required: false, autoComplete: 'country-name', col: 'half', options: GI_ZF_COUNTRIES, placeholder: '-Select-' }, ]; default: return []; } }; const allSpecs = (form) => form.fields.reduce((a, f) => a.concat(fieldSpecs(f)), []); // =================================================================== // Helpers // =================================================================== const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const isValidPhone = (phone) => { const d = (phone || '').replace(/\D/g, ''); return d.length === 10 || (d.length === 11 && d.startsWith('1')); }; const validateSpec = (spec, raw) => { const val = (raw == null ? '' : String(raw)).trim(); if (spec.kind === 'select') { if (spec.required && (!val || val === spec.placeholder)) return 'Please select an option'; return null; } if (spec.required && !val) return 'This field is required'; if (val && spec.kind === 'email' && !EMAIL_RE.test(val)) return 'Please enter a valid email address'; if (val && spec.kind === 'tel' && !isValidPhone(val)) return 'Please enter a valid phone number'; return null; }; // Mirror the hosted iframe's referrername logic: prefer the true top URL, // guard the cross-origin throw, cap length. const referrerUrl = () => { let url = ''; try { url = (window.top && window.top.location && window.top.location.href) || window.location.href; } catch (e) { url = window.location.href; } if (typeof url !== 'string') url = ''; return url.slice(0, 1800); }; // =================================================================== // THE ENGINE — state + validation + native iframe POST + referrer. // Presentation-agnostic; both modal and inline wrappers consume it. // =================================================================== const useGiZohoForm = (form) => { const specs = React.useMemo(() => allSpecs(form), [form.key]); const [values, setValues] = React.useState(() => { const o = {}; specs.forEach((s) => { o[s.name] = s.kind === 'select' ? (s.placeholder || '') : ''; }); return o; }); const [errors, setErrors] = React.useState({}); const [sending, setSending] = React.useState(false); const [submitted, setSubmitted] = React.useState(false); const [formError, setFormError] = React.useState(null); const formRef = React.useRef(null); const pendingRef = React.useRef(false); const timeoutRef = React.useRef(null); const sinkName = 'gi-zf-sink-' + form.key; React.useEffect(() => () => { if (timeoutRef.current) clearTimeout(timeoutRef.current); }, []); const setField = (name) => (e) => { const v = e.target.value; setValues((p) => ({ ...p, [name]: v })); setErrors((p) => (p[name] ? { ...p, [name]: null } : p)); }; const onSubmit = (e) => { if (sending) { e.preventDefault(); return; } const next = {}; let first = null; specs.forEach((s) => { const err = validateSpec(s, values[s.name]); if (err) { next[s.name] = err; if (!first) first = s.name; } }); setErrors(next); if (Object.keys(next).length) { e.preventDefault(); const node = formRef.current && formRef.current.querySelector('[name="' + first + '"]'); if (node) node.focus(); return; } if (typeof navigator !== 'undefined' && navigator.onLine === false) { e.preventDefault(); setFormError('You appear to be offline. Please check your connection and try again.'); return; } if (window.GIGtm && typeof window.GIGtm.generateLead === 'function') { window.GIGtm.generateLead({ form_id: form.gtmFormId || form.key, form_location: window.GI_PAGE || 'site' }); } // Valid → let the native multipart POST flow to the hidden iframe. setFormError(null); setSending(true); pendingRef.current = true; timeoutRef.current = setTimeout(() => { if (!pendingRef.current) return; pendingRef.current = false; setSending(false); setFormError('Something went wrong sending your request. Please try again, or email us directly.'); }, 15000); }; const onSinkLoad = () => { if (!pendingRef.current) return; // ignore initial about:blank pendingRef.current = false; if (timeoutRef.current) clearTimeout(timeoutRef.current); setSending(false); setSubmitted(true); }; return { values, errors, sending, submitted, formError, specs, setField, onSubmit, onSinkLoad, sinkName, formProps: { ref: formRef, action: form.action, method: 'post', encType: 'multipart/form-data', acceptCharset: 'UTF-8', target: sinkName, onSubmit, noValidate: true }, }; }; // Hidden Zoho control inputs (verbatim names) + the off-screen sink iframe. const HiddenControls = () => ( ); const Sink = ({ eng }) => (