'use strict';
(function () {
// Configuration
var CONFIG = {
IFRAME_URL: 'https://go.nowcircular.com/widget',
CIRCULAR_LOGO: 'https://assets.nowcircular.sg/images/circular-logo.png',
API_BASE_URL: 'https://nowcircular.sg/api/v1',
DEBOUNCE_DELAY: 200, // Debounce delay in milliseconds
};
// Primary DOM Elements
var widgetContainer = document.getElementById('circular-on-site-widget');
// State
var banner = null;
var modal = null;
var observer = null;
// Main execution
function init() {
initStyles();
modal = createModal();
fetchPricing(initWidget, handleError);
var keyEvents = bindKeyEvents();
initAttributeObserver();
// Cleanup
window.addEventListener('unload', cleanup(keyEvents));
}
function cleanup(keyEvents) {
return function () {
if (banner) {
banner.unload();
}
if (modal) {
modal.unload();
}
if (keyEvents) {
keyEvents.unload();
}
if (observer) {
observer.disconnect();
}
};
}
// Widget initialization
function initWidget(productPricingData) {
if (widgetContainer) {
if (banner) {
banner.update(productPricingData);
} else {
banner = createBanner(productPricingData);
widgetContainer.innerHTML = ''; // Clear any error messages
widgetContainer.appendChild(banner.element);
}
}
}
function handleError() {
console.warn('Circular Widget: Failed to initialize widget.');
if (widgetContainer) {
widgetContainer.innerHTML = ''; // Clear the widget container
banner = null; // Reset the banner state
}
}
// Banner creation and management
function createBanner(productPricingData) {
var bannerElement = document.createElement('div');
bannerElement.className = 'circular-banner';
function render(data) {
var monthlyPrice = data.monthlyPrice;
var durationMonths = data.durationMonths;
const content = `
Subscribe from $${monthlyPrice}/month for ${durationMonths} months
Powered by
`;
bannerElement.innerHTML = content;
}
function update(newProductPricingData) {
render({
monthlyPrice: formatPrice(newProductPricingData.monthly_price_cents / 100),
durationMonths: newProductPricingData.duration_months || '24',
});
}
update(productPricingData);
bannerElement.addEventListener('click', modal.openModal);
return {
element: bannerElement,
update: update,
unload: function () {
bannerElement.removeEventListener('click', modal.openModal);
},
};
}
// Data fetching
function fetchPricing(success, error) {
var attributes = getWidgetAttributes();
var sku = attributes.sku;
var partner = attributes.partner;
var fullPurchasePrice = attributes.fullPurchasePrice;
if (!sku || !partner || !fullPurchasePrice) {
console.warn('Circular Widget: Missing data attributes.');
error();
return;
}
xhrGet({
url: getPricingUrl(partner, sku),
success: success,
error: function () {
console.warn('Circular Widget: No SKU mapping found for partner: ' + partner + ' and SKU: ' + sku);
error();
},
});
}
function getWidgetAttributes() {
return {
sku: widgetContainer ? widgetContainer.getAttribute('data-sku') : null,
partner: widgetContainer ? widgetContainer.getAttribute('data-partner') : null,
fullPurchasePrice: widgetContainer ? widgetContainer.getAttribute('data-full-purchase-price') : null,
};
}
// Attribute change observer
function initAttributeObserver() {
if (!widgetContainer) return;
var debouncedFetchPricing = debounce(function () {
fetchPricing(initWidget, handleError);
}, CONFIG.DEBOUNCE_DELAY);
observer = new MutationObserver(function (mutations) {
var hasRelevantChanges = mutations.some(function (mutation) {
return (
mutation.type === 'attributes' && ['data-sku', 'data-partner', 'data-full-purchase-price'].indexOf(mutation.attributeName) !== -1
);
});
if (hasRelevantChanges) {
debouncedFetchPricing();
}
});
var config = {attributes: true, attributeFilter: ['data-sku', 'data-partner', 'data-full-purchase-price']};
observer.observe(widgetContainer, config);
}
// Modal functions
function createModal() {
var modalElement = document.createElement('div');
modalElement.id = 'circular-on-site-modal';
var spinner = document.createElement('div');
spinner.className = 'circular-spinner';
var iframe = document.createElement('iframe');
modalElement.appendChild(spinner);
modalElement.appendChild(iframe);
document.body.appendChild(modalElement);
function openModal() {
iframe.style.display = 'none';
spinner.style.display = 'block';
iframe.src = CONFIG.IFRAME_URL;
modalElement.classList.add('active');
}
function closeModal() {
modalElement.classList.remove('active');
iframe.removeAttribute('src');
spinner.style.display = 'block';
}
function iframeLoad() {
spinner.style.display = 'none';
iframe.style.display = 'block';
}
iframe.addEventListener('load', iframeLoad);
modalElement.addEventListener('click', closeModal);
return {
openModal: openModal,
closeModal: closeModal,
unload: function () {
iframe.removeEventListener('load', iframeLoad);
modalElement.removeEventListener('click', closeModal);
},
};
}
// Event binding
function bindKeyEvents() {
function handleEscapeKey(event) {
if (event.key === 'Escape') {
modal.closeModal();
}
}
document.addEventListener('keydown', handleEscapeKey);
return {
unload: function () {
document.removeEventListener('keydown', handleEscapeKey);
},
};
}
// Utility functions
function initStyles() {
var style = document.createElement('style');
style.innerHTML = `
@import url('https://fonts.googleapis.com/css2?family=Lexend+Deca:wght@400;500&display=swap');
#circular-on-site-widget {
font-family: 'Lexend Deca', sans-serif;
}
#circular-on-site-widget .circular-banner {
padding: 12px 16px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
border-radius: 0px 5px 5px 0px;
border-left: 2px solid #EA2E5D;
background: #F6F4FC;
}
#circular-on-site-widget .subscription-content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 8px;
flex-grow: 1;
}
#circular-on-site-widget .circular-banner h2 {
color: #190644;
font-size: 14px;
font-weight: 500;
line-height: 16px;
margin: 0 0 4px 0;
}
#circular-on-site-widget .subscription-details {
display: flex;
gap: 4px;
}
#circular-on-site-widget .subscription-details span {
color: #5E537C;
font-size: 12px;
font-weight: 400;
line-height: 15px;
}
#circular-on-site-widget .subscription-details a {
color: #EA2F5D;
font-size: 12px;
font-weight: 400;
line-height: 15px;
text-decoration: underline;
}
#circular-on-site-widget .powered-by {
display: flex;
align-items: center;
gap: 4px;
color: #5E537C;
font-size: 9px;
font-weight: 500;
line-height: 15px;
letter-spacing: -0.27px;
align-self: flex-start;
flex-shrink: 0;
order: 1;
}
#circular-on-site-widget .circular-logo {
width: 50px;
height: auto;
}
#circular-on-site-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
visibility: hidden;
z-index: 100;
}
#circular-on-site-modal iframe {
width: 80%;
max-width: 610px;
height: 80%;
border: none;
}
#circular-on-site-modal.active {
visibility: visible;
}
#circular-on-site-modal .circular-spinner {
border: 4px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top: 4px solid #ffffff;
width: 40px;
height: 40px;
animation: circular-spin 2s linear infinite;
}
@keyframes circular-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
}
function xhrGet(params) {
var request = new XMLHttpRequest();
request.open('GET', params.url, true);
request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
request.onreadystatechange = function () {
if (request.readyState === 4) {
if (request.status >= 200 && request.status < 300) {
params.success(parse(request));
} else if (params.error) {
params.error(parse(request));
}
}
};
request.send();
}
function parse(req) {
try {
return JSON.parse(req.responseText);
} catch (e) {
return req.responseText;
}
}
function getPricingUrl(partner, sku) {
return CONFIG.API_BASE_URL + '/partners/' + partner + '/sku/' + sku + '/lowest-price';
}
function formatPrice(value) {
if (value === undefined) return '';
return Number(value).toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
}
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments;
var later = function () {
timeout = null;
func.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
init();
})();