npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@roofmaxx/form

v0.103.0

Published

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

Readme

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/[email protected]/dist/form.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@roofmaxx/[email protected]/dist/form.css" />

-> STEP 2 — Register your domain

Please send an email to [email protected] 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.
  • On blur (when the user leaves the ZIP field), validateZipExists() calls the Zippopotam.us API to confirm the ZIP is a real US postal code. An inline error is shown immediately if not.
  • 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) — called on blur in Step 2. 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 | | detected_meeting_link | "true" / "false" — whether the looked-up dealer has a meeting link | | 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_brand_name | string | Dealer's brand name (if a dealer was found) | | 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 |