Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 21 additions & 59 deletions astro/src/components/BlogNewsletterCTA.astro
Original file line number Diff line number Diff line change
@@ -1,62 +1,24 @@
---
---
<script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/634739.js"></script>

<script type="module">
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('myActualForm');
const hsContextInput = document.getElementById('hs_context');

if (!form || !hsContextInput) return;

function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : null;
}

function setHubSpotContext() {
const hutk = getCookie('hubspotutk');
if (!hutk) {
console.warn('HubSpot tracking cookie not found.');
return;
}

const hsContext = {
hutk,
pageUrl: window.location.href,
pageName: document.title
};

hsContextInput.value = JSON.stringify(hsContext);
}

form.addEventListener('submit', () => {
setHubSpotContext();

const formContents = form.querySelector('div.flex.gap-x-4');
if (formContents) {
formContents.style.display = 'none';
}

if (!form.querySelector('.thank-you-message')) {
const thankYouMessage = document.createElement('p');
thankYouMessage.textContent = 'Thank you for your submission!';
thankYouMessage.className = 'thank-you-message text-slate-600 mt-4';
form.appendChild(thankYouMessage);
}
});
});
<script>
import HubSpotForm from "src/components/HubspotForm";
new HubSpotForm("#blog-newsletter-cta","b2983217-5f06-4537-a54c-18213b260d5d");
</script>

<div class="hidden mb-10 lg:block">
<hr class="bg-slate-200 border-t-0 h-0.5 mb-2"/>
<p class="font-inter leading-6 mb-3 sm:text-base md:leading-7 md:text-lg lg:text-base">Get the best of FusionAuth. Once a month. Directly
to your inbox.</p>
<form id="myActualForm" action="https://forms.hubspot.com/uploads/form/v2/634739/b2983217-5f06-4537-a54c-18213b260d5d" method="POST" target="_self" class="margin-top: 25px;">
<div class="flex gap-x-4 ">
<input type="email" name="EMAIL" size="30" placeholder="Email address *" required class="min-w-0 flex-auto rounded-md border-0 bg-slate-100 px-3.5 py-2 text-slate-600 shadow-sm ring-1 ring-inset ring-white/10 focus:ring-2 focus:ring-inset focus:ring-indigo-500 sm:text-sm sm:leading-6 placeholder:text-gray-500" />
<input type="hidden" name="hs_context" id="hs_context" />
<button type="submit" class="flex-none rounded-md bg-indigo-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500">Submit</button>
</div>
</form>
</div>
<hr class="bg-slate-200 border-t-0 h-0.5 mb-2" />
<p class="font-inter leading-6 mb-3 sm:text-base md:leading-7 md:text-lg lg:text-base">
Get the best of FusionAuth. Once a month. Directly to your inbox.
</p>
<form data-hubspot-form id="blog-newsletter-cta" class="space-y-4">
<div class="error-container mb-4" style="display:none;"></div>
<div class="form-fields flex gap-x-4">
<input type="email" name="EMAIL" size="30" placeholder="Email address *" aria-label="Email address" required
class="min-w-0 flex-auto rounded-md border-0 bg-slate-100 px-3.5 py-2 text-slate-600 shadow-sm ring-1 ring-inset ring-white/10 focus:ring-2 focus:ring-inset focus:ring-indigo-500 sm:text-sm sm:leading-6 placeholder:text-gray-500" />
<input type="hidden" name="hs_context" id="hs_context" />
<button type="submit"
class="flex-none rounded-md bg-indigo-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500">
Submit
</button>
</div>
<div class="success-message mt-4" style="display:none;"></div>
</form>
</div>
278 changes: 278 additions & 0 deletions astro/src/components/HubspotForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
/**
* HubSpot Custom Form Handler
* Handles form submission using HubSpot's Forms API with custom HTML.
* Allows multiple HTML forms on a page to be managed independently,
* even if they submit to the same or different HubSpot Form GUIDs.
*
* Each HTML form is identified by a unique client-side selector,
* and the HubSpot Form GUID for submission is passed separately.
*/

const HUBSPOT_PORTAL_ID = '634739';

class HubSpotForm {
/**
* @param {string} htmlFormSelector - A CSS selector to uniquely identify the HTML form element (e.g., '#myForm', 'form[data-unique-id="footer-contact"]').
* @param {string} hubSpotFormGuid - The GUID of the HubSpot form to submit data to.
*/
constructor(htmlFormSelector, hubSpotFormGuid) {
this.portalId = HUBSPOT_PORTAL_ID;
this.hubSpotFormGuid = hubSpotFormGuid; // GUID for the HubSpot API endpoint
this.apiUrl = `https://api.hsforms.com/submissions/v3/integration/submit/${this.portalId}/${this.hubSpotFormGuid}`;

// Find the specific HTML form element using the provided selector
this.htmlFormElement = document.querySelector(htmlFormSelector);

if (!this.htmlFormElement) {
// console.warn(`HubSpotForm: No HTML form found with selector "${htmlFormSelector}". This instance will not initialize.`);
return; // Do not proceed if the specific form is not found
}

// Ensure a generic 'data-hubspot-form' attribute exists for general targeting if needed.
this.htmlFormElement.setAttribute('data-hubspot-form', '');

this.init(); // Call init only if the specific form element is found
}

init() {
this.loadTrackingScript();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.setupFormHandlers());
} else {
this.setupFormHandlers();
}
}

loadTrackingScript() {
if (!document.querySelector('#hs-script-loader')) {
const script = document.createElement('script');
script.id = 'hs-script-loader';
script.src = `//js.hs-scripts.com/${HUBSPOT_PORTAL_ID}.js`;
script.async = true;
script.defer = true;
document.head.appendChild(script);
}
}

setupFormHandlers() {
if (this.htmlFormElement) {
this.htmlFormElement.addEventListener('submit', (e) => this.handleSubmit(e));
}
}

getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : null;
}

getFormData(form) {
const formData = new FormData(form);
const fields = [];
for (let [name, value] of formData.entries()) {
if (name !== 'submit') {
fields.push({
objectType: name.toLowerCase() === 'email' ? 'CONTACT' : undefined,
name: name,
value: value
});
}
}
return fields;
}

getContext() {
return {
hutk: this.getCookie('hubspotutk'),
pageUri: window.location.href,
pageName: document.title
};
}

isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(String(email).toLowerCase());
}

async handleSubmit(event) {
event.preventDefault();

const form = event.target; // This is this.htmlFormElement
const submitButton = form.querySelector('[type="submit"]');
const originalButtonText = submitButton.textContent;

submitButton.disabled = true;
submitButton.textContent = 'Submitting...';
this.clearErrors(form);

const formFieldsData = this.getFormData(form);
let clientSideErrors = [];

const emailFieldData = formFieldsData.find(field => field.name.toLowerCase() === 'email');
if (emailFieldData) {
if (!emailFieldData.value) {
clientSideErrors.push({ message: 'Email address is required.' });
} else if (!this.isValidEmail(emailFieldData.value)) {
clientSideErrors.push({ message: `Enter a valid email address.` });
}
}

if (clientSideErrors.length > 0) {
this.handleError(form, {
message: 'Please correct the issues below:',
errors: clientSideErrors
});
submitButton.disabled = false;
submitButton.textContent = originalButtonText;
return;
}

try {
const context = this.getContext();
const payload = {
fields: formFieldsData,
context: context
};

// API URL uses this.hubSpotFormGuid passed in constructor
const response = await fetch(this.apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});

if (response.ok) {
this.handleSuccess(form);
} else {
const errorData = await response.json();
if (errorData.message && !errorData.errors) {
this.handleError(form, { errors: [{ message: errorData.message }] });
} else {
this.handleError(form, errorData);
}
}
} catch (error) {
console.error('Form submission error:', error);
this.handleError(form, { message: 'Network error. Please try again.' });
}

submitButton.disabled = false;
submitButton.textContent = originalButtonText;
}

handleSuccess(form) {
const formFieldsContainer = form.querySelector('.form-fields');
if (formFieldsContainer) {
formFieldsContainer.style.display = 'none';
} else {
const allInputs = form.querySelectorAll('input:not([type="submit"]), textarea, select');
allInputs.forEach(f => {
if(f.parentElement) f.parentElement.style.display = 'none';
});
}
const submitButton = form.querySelector('[type="submit"]');
if (submitButton) {
submitButton.style.display = 'none';
}

let successMessage = form.querySelector('.success-message');
if (!successMessage) {
successMessage = document.createElement('div');
successMessage.className = 'success-message mt-4';
form.appendChild(successMessage);
}
successMessage.innerHTML = `
<div class="border border-green-200 rounded-md p-2">
<div class="flex">
<div class="ml-3">
<p class="text-sm font-medium">
Thank you! Your submission has been received.
</p>
</div>
</div>
</div>
`;
successMessage.style.display = 'block';
}

handleError(form, errorData) {
let errorContainer = form.querySelector('.error-container');
if (!errorContainer) {
errorContainer = document.createElement('div');
errorContainer.className = 'error-container mb-4';
form.insertBefore(errorContainer, form.firstChild);
}

let errorMessagesHTML = '';
const uniqueMessages = new Set();

if (errorData && errorData.message && (!errorData.errors || errorData.errors.length === 0)) {
uniqueMessages.add(errorData.message);
} else if (errorData && errorData.message) {
errorMessagesHTML += `<p class="text-sm font-medium mb-2">${errorData.message}</p>`;
}

if (errorData && errorData.errors && errorData.errors.length > 0) {
errorMessagesHTML += '<ul class="list-disc list-inside pl-1 text-sm">';
errorData.errors.forEach(error => {
let message = error.message;
if (error.path) {
const fieldNameFromPath = error.path.split('.').pop();
if (message.includes("VALIDATION_ERROR_MESSAGE") && fieldNameFromPath) {
message = `Invalid input for ${fieldNameFromPath.replace(/([A-Z])/g, ' $1').toLowerCase()}.`;
} else if (fieldNameFromPath && !message.toLowerCase().includes(fieldNameFromPath.toLowerCase())) {
message = `${fieldNameFromPath.charAt(0).toUpperCase() + fieldNameFromPath.slice(1)}: ${message}`;
}
} else if (error.name && !message.toLowerCase().includes(error.name.toLowerCase())) {
message = `${error.name.charAt(0).toUpperCase() + error.name.slice(1)}: ${message}`;
}

if (!uniqueMessages.has(message)) {
errorMessagesHTML += `<li>${message}</li>`;
uniqueMessages.add(message);
}
});
errorMessagesHTML += '</ul>';
} else if (uniqueMessages.size > 0) {
errorMessagesHTML += '<ul class="list-disc list-inside pl-1 text-sm">';
uniqueMessages.forEach(msg => {
errorMessagesHTML += `<li>${msg}</li>`;
});
errorMessagesHTML += '</ul>';
}

if (errorMessagesHTML.trim() === '' && uniqueMessages.size === 0) {
errorMessagesHTML = `<p class="text-sm font-medium">An unexpected error occurred. Please try again.</p>`;
}

errorContainer.innerHTML = `
<div class="border border-red-400 rounded-md p-2">
<div class="flex">
<div class="ml-3">
${errorMessagesHTML}
</div>
</div>
</div>
`;
errorContainer.style.display = 'block';
}

clearErrors(form) {
const errorContainer = form.querySelector('.error-container');
if (errorContainer) {
errorContainer.innerHTML = '';
errorContainer.style.display = 'none';
}
}
}

export default HubSpotForm;

// Convenience function to quickly initialize a form
// Now expects the HTML form selector and the HubSpot Form GUID
export function initHubSpotForm(htmlFormSelector, hubSpotFormGuid) {
if (!htmlFormSelector || !hubSpotFormGuid) {
// console.error("initHubSpotForm: Both htmlFormSelector and hubSpotFormGuid are required.");
return null;
}
return new HubSpotForm(htmlFormSelector, hubSpotFormGuid);
}
Loading