npm.io
0.115.0 • Published 3 weeks ago

@roofmaxx/form

Licence
MIT
Version
0.115.0
Deps
14
Size
573 kB
Vulns
0
Weekly
0

Roof Maxx ReactJs Form

This document covers both how to install and configure the form on any website and how the form works internally in full technical detail.

The form is a multistep lead-capture widget built with ReactJS + Tailwind CSS, integrated with three external services:

  • HubSpot Forms API — lead submission and CRM
  • Roof Maxx Connect map_status API — real-time dealer lookup by location
  • Google Maps Places API — address autocomplete with city/state/ZIP extraction
  • Zippopotam.us API — ZIP code existence validation

Table of Contents

  1. Installation
  2. Usage & Configuration
  3. UTM Priority Logic
  4. Examples of Use
  5. How the Form Works — Step by Step
  6. Conditional Field Logic
  7. State & Data Persistence
  8. Dealer Finder Logic
  9. HubSpot Submission
  10. Redirect Logic
  11. Google Maps Integration
  12. Component Architecture
  13. Styling & Theming
  14. Build & Development
  15. Third-Party Services Reference

Installation

-> STEP 1 — Add the script tag to your page
Latest Files

Copy and paste the codes below preferably in the <head> of your page:

<script type="module" src="https://unpkg.com/@roofmaxx/form/dist/form.js"></script>

If you are facing issues with the form try inserting the JS script (not the CSS) in the footer of your page OR add the defer value (defer means the script executes after the document has been parsed)

<script defer type="module" src="https://unpkg.com/@roofmaxx/form/dist/form.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@roofmaxx/form/dist/form.css" />
Specific Version Files

To pin a specific version, specify it in both the script and link tags:

<script type="module" src="https://unpkg.com/@roofmaxx/form@0.3.0/dist/form.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@roofmaxx/form@0.3.0/dist/form.css" />
-> STEP 2 — Register your domain

Please send an email to techsupport@roofmaxx.com with your base domain where the form will be installed. For example if you have the form installed at www.domain.com/test or www.test.domain.com please send us domain.com.


Usage & Configuration

Place the following HTML at the location on your page where you want the form to appear:

<div id="roofmaxx-form"
    data-utm-source=""
    data-utm-campaign=""
    data-utm-medium=""
    data-utm-content=""
    data-utm-term=""
    data-page-variant=""
    data-zip-code=""
    data-dealer-id=""
    data-source-vendor=""
    data-deal-type="" 
    data-self-generated=""
    data-lead-type=""
    data-microsite-type=""
    data-thank-you-page=""
    data-sorry-page=""
    data-video-form=""
></div>

All configuration is passed via data-* attributes on the wrapper element. No JavaScript configuration is required.

Data Attributes Reference
Attribute Purpose Default
data-dealer-id Force a specific dealer ID (skips the API location lookup) —
data-source-vendor Name of the marketing vendor (e.g. "XYZ") —
data-thank-you-page Full URL to redirect to after a successful submission https://roofmaxx.com/thank-you
data-sorry-page Full URL to redirect to when no dealer is nearby (out-of-area). If empty, the thank-you page is used in all cases —
data-video-form Set to "false" to hide the Wistia intro video. Video is shown by default. "true"
data-zip-code Pre-fill the ZIP code field in Step 2 —
data-deal-type Deal type code forwarded to the dealer API and HubSpot (e.g. "RMCL-F", "MICRO") —
data-lead-type Lead type code forwarded to HubSpot "C"
data-program-type Program type code forwarded to HubSpot —
data-self-generated Self-generated lead flag (e.g. "SG" for dealer sites) —
data-microsite-type Microsite type forwarded to HubSpot "Non-Microsite"
data-page-variant Page variant code forwarded to HubSpot —
data-utm-source UTM source (see UTM priority logic below) —
data-utm-campaign UTM campaign —
data-utm-medium UTM medium —
data-utm-content UTM content —
data-utm-term UTM term —

UTM Priority Logic

The form applies the following priority order when collecting UTM parameters:

  1. Both URL and data-attributes present → data-attributes win
  2. Only URL → URL UTMs are used
  3. Only data-attributes → data-attributes are used
  4. Neither → empty values are sent

Examples of Use


Internal Use — Roof Maxx Corporate
For roofmaxx.com (Website)

In all cases (dealer found or not) the user is redirected to roofmaxx.com/thank-you.

<div id="roofmaxx-form"
    data-utm-source=""
    data-utm-campaign=""
    data-utm-medium=""
    data-utm-content=""
    data-utm-term=""
    data-page-variant=""
    data-zip-code=""
    data-dealer-id=""
    data-source-vendor=""
    data-deal-type="" 
    data-self-generated=""
    data-lead-type=""
    data-microsite-type=""
    data-thank-you-page=""
    data-sorry-page=""
    data-video-form=""
></div>
For roofmaxx.com/find-a-dealer/state OR roofmaxx.com/find-a-dealer/state/microsite

In all cases the user is redirected to roofmaxx.com/find-a-dealer/thank-you.

For the find-a-dealer / state-location page:

<div
    id="roofmaxx-form"
    data-utm-source=""
    data-utm-campaign=""
    data-utm-medium=""
    data-utm-content=""
    data-utm-term=""
    data-page-variant=""
    data-zip-code=""
    data-dealer-id=""
    data-source-vendor=""
    data-deal-type="RMCL-F"
    data-self-generated=""
    data-lead-type=""
    data-microsite-type=""
    data-thank-you-page=""
    data-sorry-page=""
    data-video-form=""
></div>
<script>
    /* Add dealer/company ID to HS form */
    if (typeof roofmaxx_company_id !== "undefined" && roofmaxx_company_id) {
        $("#roofmaxx-form").attr("data-dealer-id", roofmaxx_company_id);
    }
</script>

For Microsites:

<div
    id="roofmaxx-form"
    data-utm-source=""
    data-utm-campaign=""
    data-utm-medium=""
    data-utm-content=""
    data-utm-term=""
    data-page-variant=""
    data-zip-code=""
    data-dealer-id=""
    data-source-vendor=""
    data-deal-type="MICRO"
    data-self-generated=""
    data-lead-type=""
    data-microsite-type=""
    data-thank-you-page=""
    data-sorry-page=""
    data-video-form=""
></div>
<script>
    /* Add dealer/company ID to HS form */
    if (typeof roofmaxx_company_id !== "undefined" && roofmaxx_company_id) {
        $("#roofmaxx-form").attr("data-dealer-id", roofmaxx_company_id);
    }
</script>

Internal Use — HubSpot Landing Pages

1. Corporate landing page — same redirect regardless of dealer:

<div id="roofmaxx-form"
    data-utm-source=""
    data-utm-campaign=""
    data-utm-medium=""
    data-utm-content=""
    data-utm-term=""
    data-page-variant="" 
    data-zip-code=""
    data-dealer-id=""
    data-source-vendor=""
    data-deal-type="" 
    data-self-generated=""
    data-lead-type=""
    data-microsite-type=""
    data-thank-you-page="https://exampleHubspot.com/thank-you"
    data-sorry-page=""
    data-video-form=""
></div>

2. Corporate landing page — different pages for dealer found vs. not found:

<div id="roofmaxx-form"
    data-utm-source=""
    data-utm-campaign=""
    data-utm-medium=""
    data-utm-content=""
    data-utm-term=""
    data-page-variant="" 
    data-zip-code=""
    data-dealer-id=""
    data-source-vendor=""
    data-deal-type="" 
    data-self-generated=""
    data-lead-type=""
    data-microsite-type=""
    data-thank-you-page="https://exampleHubspotYES.com/thank-you"
    data-sorry-page="https://exampleHubspotNO.com/thank-you"
    data-video-form=""
></div>

3. Vendor (XYZ) landing page — same redirect regardless of dealer:

<div id="roofmaxx-form"
    data-utm-source=""
    data-utm-campaign=""
    data-utm-medium=""
    data-utm-content=""
    data-utm-term=""
    data-page-variant="" 
    data-zip-code=""
    data-dealer-id=""
    data-source-vendor="XYZ"
    data-deal-type="" 
    data-self-generated=""
    data-lead-type=""
    data-microsite-type=""
    data-thank-you-page="https://exampleHubspot.com/thank-you"
    data-sorry-page=""
    data-video-form=""
></div>

4. Vendor (XYZ) landing page — different pages for dealer found vs. not found:

<div id="roofmaxx-form"
    data-utm-source=""
    data-utm-campaign=""
    data-utm-medium=""
    data-utm-content=""
    data-utm-term=""
    data-page-variant="" 
    data-zip-code=""
    data-dealer-id=""
    data-source-vendor="XYZ"
    data-deal-type="" 
    data-self-generated=""
    data-lead-type=""
    data-microsite-type=""
    data-thank-you-page="https://exampleHubspotYES.com/thank-you"
    data-sorry-page="https://exampleHubspotNO.com/thank-you"
    data-video-form=""
></div>

Dealer Use

Placing the form on a dealer's own website or landing page with a custom thank-you page.

It is recommended to always supply a custom data-thank-you-page. Without one the user is redirected to roofmaxx.com/thank-you and conversion tracking on the dealer's own site will not work correctly.

  • In case you use UTM parameters and you don't want to use the UTMs in the URL, insert them as data-attributes. These hardcoded UTM parameters are only sent when the form is submitted.
  • Dealer ID example: 2452308196
<div id="roofmaxx-form"
    data-utm-source=""
    data-utm-campaign=""
    data-utm-medium=""
    data-utm-content=""
    data-utm-term=""
    data-page-variant="" 
    data-zip-code=""
    data-dealer-id="2452308196"
    data-source-vendor=""
    data-deal-type="" 
    data-self-generated="SG"
    data-lead-type=""
    data-microsite-type=""
    data-thank-you-page="https://dealers.com/thank-you"
    data-sorry-page=""
    data-video-form=""
></div>

How the Form Works — Step by Step

The form is a React application that mounts inside #roofmaxx-form and guides the user through three steps before submitting a lead to HubSpot.

Step 1 — Roof Information

Collects details about the property and roof condition.

Question 1 — Property Type (always shown, required)

Option Internal Value
Single-Family Home SingleFamilyHome
Condominium / Apartment MultiFamily
Commercial Commercial

Question 2 — Manage Multiple Properties? (shown only when MultiFamily or Commercial is selected)

Option Internal Value
Yes Yes
No No

Question 3 — Property Unit Count (shown only when Question 2 is Yes)

Option Internal Value
1–10 units 1To10
11–50 units 11To50
51+ units 51Plus
Not Sure Unknown

Question 4 — Asphalt Shingle Roof? (always shown, required)

Option Internal Value
Yes Yes
No No
Not Sure NotSure

Question 5 — Roof Age (always shown, required)

Option Internal Value
0–4 years ZeroToFour
5–15 years FiveToFifteen
15+ years FifteenPlus
Not Sure Unknown

On submit the answers are saved to localStorage and the form advances to Step 2.


Step 2 — Location

Collects the property address to power the dealer lookup.

Field Type Notes
Street Address Text (autocomplete) Google Maps Places API — selecting a suggestion auto-fills city, state, and ZIP
ZIP Code Text Exactly 5 numeric digits, required. Non-numeric characters are blocked as you type. Validated against Zippopotam.us on blur.
City Text Auto-filled from address selection, or entered manually
State Dropdown All 50 US states + DC

ZIP code validation:

  • Non-numeric characters are stripped in real time as the user types.
  • validateZipExists() calls the Zippopotam.us API to confirm the ZIP is a real US postal code. It is registered as a react-hook-form validate rule, so it runs when Step 2 is submitted and blocks advancing to Step 3 — regardless of whether the ZIP was typed, filled by address autocomplete, or prefilled via data-zip-code. Results are cached per ZIP.
  • On submit, a format guard (/^\d{5}$/) in findDealer() acts as a final fallback.
  • If Zippopotam.us is unreachable, the check is skipped silently so valid submissions are never blocked.

Error messages:

  • Wrong format (non-numeric or not 5 digits): "Please enter a valid 5-digit ZIP code"
  • Valid format but ZIP does not exist: "ZIP code not found. Please enter a valid US ZIP code"

On submit:

  • If data-dealer-id was provided, the location lookup is skipped.
  • Otherwise, findDealer(location, dealTypeCode) is called against the Roof Maxx Connect API.
  • The returned dealer and LocationStatus are saved to localStorage for use on the thank-you page.
  • The dealer's ID from the lookup is not submitted to HubSpot — only a dealer_id set explicitly via data-dealer-id or ?dealer_id= is sent. This prevents pre-assigning a dealer before downstream routing completes.
  • The form advances to Step 3.

Step 3 — Contact Information

Collects the lead's personal details.

Field Type Validation
First Name Text Required
Last Name Text Required
Email Email Required, regex validated
Phone Masked text Required, format (###) ###-####, area code must start with 2–9
SMS Opt-In Checkbox Optional

On submit:

  1. Combines all data from localStorage (roof info, location, dealer) with the contact fields.
  2. Builds the HubSpot payload (see HubSpot Submission).
  3. Posts the lead to the HubSpot API.
  4. Redirects the user (see Redirect Logic).
  5. Clears all form-related localStorage keys (resetForm).

Conditional Field Logic

All conditional display logic is driven by react-hook-form's watch() in roof-information-step.tsx:

propertyType === MultiFamily OR Commercial
  → show "Manage Multiple Properties?"

  manageProperty === Yes
    → show "Property Unit Count?"

Hidden fields are also excluded from validation, so the user is never blocked by a field they cannot see.


State & Data Persistence

Form state is stored in localStorage so that progress survives a page refresh.

Key Contents
roofInformation All Step 1 answers (JSON)
location Address, ZIP, city, state from Step 2 (JSON)
dealer Dealer object returned by the API (JSON)
status LocationStatus string from the API
step Current step index: 0, 1, or 2

On successful final submission all keys are cleared via resetForm().


Dealer Finder Logic

packages/utils/src/dealers.ts exports two functions:

validateZipExists(zipCode) — registered as a validate rule on the ZIP field, so it runs on Step 2 submit. Hits the Zippopotam.us API to confirm the ZIP is a real US postal code. Returns true if valid or if the service is unreachable, false if the ZIP does not exist.

findDealer(location, dealType) — called on Step 2 submit. Contains a format guard that throws INVALID_ZIP for non-5-digit values, then calls the Roof Maxx Connect API:

GET https://roofmaxxconnect.com/api/v1/map-status
    postal_code, city, state, address, deal_type, api_token

The API responds with a LocationStatus and a Dealer object.

LocationStatus Values
Status Meaning
TERRITORY Active dealer in the user's area
CA1 Close enough — dealer covers this area
CA2 On the boundary — dealer may cover this area
HOLD Area on hold, no active dealer
ERROR API error or no response
CORPCOMMERCIAL Corporate commercial account territory

isDealerNearby() returns true only for TERRITORY and CA1. This result drives the redirect decision.

Dealer Slug Attribution

form.tsx also checks the current URL for the pattern /find-a-dealer/{state}/{city}. When found, the slug is forwarded to HubSpot as an attribution signal.


HubSpot Submission

packages/utils/src/hubspot.ts submits to:

POST https://api.hsforms.com/submissions/v3/integration/submit/6044336/{formId}
  • Portal ID: 6044336
  • Form ID: 3e57be1b-f9df-4263-9cf4-8c11d67427fd
Fields Sent to HubSpot
HubSpot Field Source
email, firstname, lastname Step 3
phone, sms_opt_in_checkbox Step 3
address, zip, city, state Step 2
property_type, roof_age, shingle_roof Step 1
manage_property, property_unit_count Step 1 (conditional)
dealer_id Only when explicitly set via data-dealer-id attribute or ?dealer_id= URL param — not from the automatic dealer lookup
had_explicit_dealer_id true if dealer_id was explicitly provided, false if it came from the lookup
utm_source, utm_campaign, utm_medium, utm_content, utm_term Data attributes / URL
steps_completed, page_variant Internal form metadata
lead_type_code, program_type_code Data attributes
microsite_type, self_generated Data attributes
deal_type_code, source_vendor Data attributes

The submission also includes:

  • The HubSpot tracking cookie (hubspotutk) for contact deduplication
  • The client's IP address (fetched asynchronously from api64.ipify.org)
  • The current page URI and title as context

Redirect Logic

After a successful HubSpot submission:

  1. A URL is built by appending all collected form fields as query parameters to data-thank-you-page.
  2. If data-sorry-page is set AND isDealerNearby() returns false → redirect to the sorry page.
  3. Otherwise → redirect to the thank-you page.
Thank-you page URL parameters

Key parameters passed to the thank-you page for routing decisions:

Parameter Value Notes
dealer_found "true" / "false" Whether a dealer is available — either explicitly assigned or found via lookup
dealer_status LocationStatus string Raw status from the dealer API
dealer_bill_com_customer_name string Dealer's Bill.com customer name (if a dealer was found) — used to route Enterprise/PMI leads
dealer_id string Only present when explicitly set via data-dealer-id or ?dealer_id=

All http:// URLs are automatically upgraded to https:// before redirecting.


Google Maps Integration

Step 2 wraps the location fields with @react-google-maps/api's LoadScript component.

The StreetInput component:

  • Uses the Google Places AutocompleteService to fetch address predictions as the user types (debounced 300 ms).
  • Renders a dropdown of predictions with keyboard navigation (arrow keys, Enter, Escape).
  • On selection, calls PlacesService.getDetails() to extract structured address components (street number, route, city, state, ZIP).
  • Auto-fills the ZIP, city, and state fields from the selected result.

Component Architecture

The project is a Turborepo monorepo with three shared packages consumed by the form app.

Monorepo Structure
roofmaxx/
├── apps/form/          # The embeddable React form application
└── packages/
    ├── components/     # Reusable UI components
    ├── types/          # Shared TypeScript types and enums
    └── utils/          # Service functions (HubSpot, dealer API, URL helpers)
Key UI Components (packages/components)
Component Description
Input Text input with error state styling
StreetInput Google Places autocomplete input
PhoneInput Masked phone input — format (###) ###-####, first digit 2–9
Select Styled select dropdown
Spinner Animated 4-dot loading indicator
IconRadioGroup / IconRadioItem Icon-based radio button groups used for Step 1 questions
StepCard Animated step container with 500 ms slide transition
StepSelector Numbered progress indicator — allows jumping back to completed steps
FormGroup, FormLabel, FormError Form layout primitives
TypeScript Types (packages/types)
Type Description
Step enum RoofInformation (0), Location (1), ContactInformation (2)
RoofInformation Step 1 answer shape
Location Step 2 address shape
Contact Step 3 contact shape
Dealer Dealer object returned by the API
LocationStatus Enum of possible dealer proximity statuses
HubSpotFields Generic field mapping for HubSpot submission

Styling & Theming

Tailwind CSS is used with a tw- class prefix to avoid collisions with the host page's styles. The important: true option is set so form styles always win over host-page styles.

Brand Color Palette
Token Hex Usage
roofmaxx-primary #003057 Dark blue — primary buttons and headers
roofmaxx-secondary #0088BB Light blue — focus states and accents
roofmaxx-accent #CC6C27 Orange — warning and info states
roofmaxx-accent2 #D68A52 Light orange
roofmaxx-accent3 #66B8D6 Cyan
Animations
Animation Duration Used by
Step slide in/out 500 ms StepCard (Headless UI Transition)
Icon hover scale 300 ms IconRadioItem
Spinner (popout/translate/popin) Custom keyframes Spinner component

Build & Development

The project uses Turborepo for task orchestration and Parcel 2 as the bundler.

Install dependencies (run from the repository root — do not run npm install in subdirectories):

npm install

Start dev server (http://localhost:3000):

npm run dev

Production build (outputs to apps/form/dist/):

npm run build

The compiled output is a global-format IIFE bundle (dist/form.js + dist/form.css). No module system is required on the host page.


Third-Party Services Reference

Service Purpose Key / Endpoint
HubSpot Forms API Lead submission Portal 6044336 · Form 3e57be1b-f9df-4263-9cf4-8c11d67427fd
Google Maps Places API Address autocomplete Key: AIzaSyA6XzSKqUYfuG97HDpQmrDZXBHVHM8vbYs
Roof Maxx Connect API Dealer lookup https://roofmaxxconnect.com/api/v1/map-status
Zippopotam.us ZIP code existence validation https://api.zippopotam.us/us/{zipcode} — free, no key required
ipify Client IP address https://api64.ipify.org?format=json
Wistia Intro video Video ID: zfdfuxftgy

To rotate any key or endpoint, update the relevant source file:

Key File
HubSpot Portal & Form ID packages/utils/src/hubspot.ts
Google Maps API Key apps/form/src/steps/location-step.tsx
Dealer API endpoint & token packages/utils/src/dealers.ts
Wistia Video ID apps/form/src/components/video-form.tsx