senangwebs-buy
Version:
Lightweight JavaScript library that that enables easy implementation of e-commerce functionality through HTML attributes
661 lines (567 loc) • 28.1 kB
JavaScript
// SenangWebs Buy Library
class SWB {
constructor() {
this.stores = new Map();
this.colors = {
primary: '#007bff',
secondary: '#dc3545'
};
this.products = [];
this.filteredProducts = [];
this.sortState = {
field: 'name',
direction: 'asc'
};
this.currency = {
code: 'USD',
symbol: '$'
};
this.init();
this.checkoutConfig = new Map();
}
init() {
this.initializeCatalogs();
this.initializeIndependentButtons();
this.setupEventListeners();
this.updateCustomProperties();
}
initializeStore(storeId, storeData = {}) {
if (!this.stores.has(storeId)) {
this.stores.set(storeId, {
cart: this.loadCartFromStorage(storeId),
info: {
name: storeData.name || storeId,
whatsapp: storeData.whatsapp || '',
cartEnabled: storeData.cartEnabled !== false,
floatingCart: storeData.floatingCart || false,
// Add new checkout configuration
checkoutTitle: storeData.checkoutTitle || 'Your Cart',
billingTitle: storeData.billingTitle || 'Billing Details',
submitButtonText: storeData.submitButtonText || 'Proceed to WhatsApp',
enableBilling: storeData.enableBilling !== false,
customFields: storeData.customFields || []
},
products: [],
filteredProducts: [],
sortState: {
field: 'name',
direction: 'asc'
}
});
}
return this.stores.get(storeId);
}
loadCartFromStorage(storeId) {
const storageKey = `swb-cart-${storeId}`;
const savedCart = localStorage.getItem(storageKey);
if (savedCart) {
try {
return JSON.parse(savedCart);
} catch (e) {
localStorage.removeItem(storageKey);
}
}
return [];
}
saveCartToStorage(storeId, cart) {
const storageKey = `swb-cart-${storeId}`;
localStorage.setItem(storageKey, JSON.stringify(cart));
}
clearCart(storeId) {
const store = this.stores.get(storeId);
if (!store) return;
store.cart = [];
this.saveCartToStorage(storeId, store.cart);
this.updateCartCount(storeId);
this.renderCart(storeId);
this.closeCheckout(storeId);
}
updateCustomProperties() {
document.documentElement.style.setProperty('--swb-color-primary', this.colors.primary);
document.documentElement.style.setProperty('--swb-color-secondary', this.colors.secondary);
const primaryRGB = this.hexToRGB(this.colors.primary);
const secondaryRGB = this.hexToRGB(this.colors.secondary);
document.documentElement.style.setProperty('--swb-color-primary-rgb', primaryRGB);
document.documentElement.style.setProperty('--swb-color-secondary-rgb', secondaryRGB);
}
hexToRGB(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `${r}, ${g}, ${b}`;
}
initializeCatalogs() {
const catalogElements = document.querySelectorAll('[data-swb-catalog]');
catalogElements.forEach(catalog => {
const storeId = catalog.getAttribute('data-swb-store-id');
if (!storeId) return;
let customFields = [];
try {
const customFieldsAttr = catalog.getAttribute('data-swb-custom-fields');
if (customFieldsAttr) {
customFields = JSON.parse(customFieldsAttr);
}
} catch (e) {
console.warn('Invalid custom fields format', e);
}
this.initializeStore(storeId, {
name: catalog.getAttribute('data-swb-store'),
whatsapp: catalog.getAttribute('data-swb-whatsapp'),
floatingCart: catalog.hasAttribute('data-swb-cart-floating') ? catalog.getAttribute('data-swb-cart-floating') : false,
cartEnabled: catalog.getAttribute('data-swb-cart') !== 'false',
checkoutTitle: catalog.getAttribute('data-swb-checkout-title') || 'Your Cart',
billingTitle: catalog.getAttribute('data-swb-billing-title') || 'Billing Details',
submitButtonText: catalog.getAttribute('data-swb-submit-text') || 'Proceed to WhatsApp',
enableBilling: catalog.getAttribute('data-swb-enable-billing') !== 'false',
customFields: customFields
});
const primaryColor = catalog.getAttribute('data-swb-color-primary');
const secondaryColor = catalog.getAttribute('data-swb-color-secondary');
if (primaryColor) this.colors.primary = primaryColor;
if (secondaryColor) this.colors.secondary = secondaryColor;
const currencyCode = catalog.getAttribute('data-swb-currency');
if (currencyCode) this.setCurrency(currencyCode);
this.createCatalogHeader(catalog, storeId);
this.initializeCatalogProducts(catalog, storeId);
});
}
initializeIndependentButtons() {
document.querySelectorAll('[data-swb-cart][data-swb-store-id]').forEach(button => {
if (!button.hasAttribute('data-swb-catalog')) {
const storeId = button.getAttribute('data-swb-store-id');
this.initializeStore(storeId, {
name: button.getAttribute('data-swb-store'),
whatsapp: button.getAttribute('data-swb-whatsapp'),
cartEnabled: true,
floatingCart: false
});
const primaryColor = button.getAttribute('data-swb-color-primary');
const secondaryColor = button.getAttribute('data-swb-color-secondary');
if (primaryColor) this.colors.primary = primaryColor;
if (secondaryColor) this.colors.secondary = secondaryColor;
const currencyCode = button.getAttribute('data-swb-currency');
if (currencyCode) this.setCurrency(currencyCode);
button.addEventListener('click', () => {
this.showCheckout(storeId);
});
const counter = button.querySelector('[data-swb-cart-count]');
if (counter) {
this.updateCartCount(storeId);
}
}
});
document.querySelectorAll('[data-swb-product-sku][data-swb-store-id]').forEach(button => {
if (!button.hasAttribute('data-swb-catalog')) {
const storeId = button.getAttribute('data-swb-store-id');
const sku = button.getAttribute('data-swb-product-sku');
const store = this.stores.get(storeId);
if (!store) {
console.warn(`Store ${storeId} not initialized. Make sure you have a cart button with store information.`);
return;
}
button.addEventListener('click', () => {
const product = {
sku: sku,
name: button.getAttribute('data-swb-product-name') || sku,
price: parseFloat(button.getAttribute('data-swb-product-price')) || 0,
quantity: 1
};
this.addToCart(storeId, product);
});
}
});
}
setCurrency(code) {
const currencies = {
USD: { code: 'USD', symbol: '$' },
EUR: { code: 'EUR', symbol: '€' },
GBP: { code: 'GBP', symbol: '£' },
JPY: { code: 'JPY', symbol: '¥' },
CNY: { code: 'CNY', symbol: '¥' },
MYR: { code: 'MYR', symbol: 'RM' },
SGD: { code: 'SGD', symbol: 'S$' },
AUD: { code: 'AUD', symbol: 'A$' },
CAD: { code: 'CAD', symbol: 'C$' },
INR: { code: 'INR', symbol: '₹' },
KRW: { code: 'KRW', symbol: '₩' },
THB: { code: 'THB', symbol: '฿' },
PHP: { code: 'PHP', symbol: '₱' },
IDR: { code: 'IDR', symbol: 'Rp' },
VND: { code: 'VND', symbol: '₫' }
};
if (currencies[code]) {
this.currency = currencies[code];
}
}
formatPrice(amount) {
return `${this.currency.symbol}${amount.toFixed(2)}`;
}
createCatalogHeader(catalog, storeId) {
const header = document.createElement('div');
header.classList.add('swb-catalog-header');
const searchBox = document.createElement('input');
searchBox.type = 'text';
searchBox.placeholder = 'Search products...';
searchBox.classList.add('swb-search-input');
searchBox.addEventListener('input', (e) => this.handleSearch(e.target.value, storeId));
const sortSelect = document.createElement('select');
sortSelect.classList.add('swb-sort-select');
sortSelect.innerHTML = `
<option value="name-asc">Name (A to Z)</option>
<option value="name-desc">Name (Z to A)</option>
<option value="price-asc">Price (Low to High)</option>
<option value="price-desc">Price (High to Low)</option>
`;
sortSelect.addEventListener('change', (e) => {
const [field, direction] = e.target.value.split('-');
this.handleSort(field, direction, storeId);
});
const headerControl = document.createElement('div');
headerControl.classList.add('swb-catalog-header-control');
headerControl.appendChild(sortSelect);
const store = this.stores.get(storeId);
const count = store.cart.reduce((total, item) => total + item.quantity, 0);
if (store.info.cartEnabled) {
const cartBtn = document.createElement('button');
cartBtn.classList.add('swb-cart-button');
if (store.info.floatingCart === 'true') {
cartBtn.classList.add('swb-cart-button-floating');
}
cartBtn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 576 512">
<path d="M528.1 301.3l47.3-208C578.8 78.3 567.4 64 552 64H159.2l-9.2-44.8C147.8 8 137.9 0 126.5 0H24C10.7 0 0 10.7 0 24v16c0 13.3 10.7 24 24 24h69.9l70.2 343.4C147.3 417.1 136 435.2 136 456c0 30.9 25.1 56 56 56s56-25.1 56-56c0-15.7-6.4-29.8-16.8-40h209.6C430.4 426.2 424 440.3 424 456c0 30.9 25.1 56 56 56s56-25.1 56-56c0-22.2-12.9-41.3-31.6-50.4l5.5-24.3c3.4-15-8-29.3-23.4-29.3H218.1l-6.5-32h293.1c11.2 0 20.9-7.8 23.4-18.7z"/>
</svg>
<span class="swb-cart-count" data-swb-cart-count>${count}</span>
`;
cartBtn.addEventListener('click', () => this.showCheckout(storeId));
headerControl.appendChild(cartBtn);
}
header.appendChild(searchBox);
header.appendChild(headerControl);
catalog.insertBefore(header, catalog.firstChild);
}
initializeCatalogProducts(catalog, storeId) {
const store = this.stores.get(storeId);
if (!store) return;
const productElements = catalog.querySelectorAll('[data-swb-product]');
store.products = Array.from(productElements).map(elem => {
return {
element: elem,
sku: elem.getAttribute('data-swb-product-sku'),
name: elem.getAttribute('data-swb-product-name'),
price: parseFloat(elem.getAttribute('data-swb-product-price'))
};
});
store.filteredProducts = [...store.products];
store.products.forEach(product => {
this.initializeProduct(product.element, storeId);
});
}
initializeProduct(productElement, storeId) {
const buttonsContainer = productElement.querySelector('[data-swb-product-buttons]');
if (!buttonsContainer) return;
const externalLink = productElement.getAttribute('data-swb-product-link');
const externalLinkTitle = productElement.getAttribute('data-swb-product-link-title');
if (externalLink && externalLinkTitle) {
const linkBtn = document.createElement('a');
linkBtn.href = externalLink;
linkBtn.target = "_blank";
linkBtn.rel = "noopener noreferrer";
linkBtn.classList.add('swb-external-link');
linkBtn.innerHTML = `<span>${externalLinkTitle}</span>`;
buttonsContainer.appendChild(linkBtn);
}
const store = this.stores.get(storeId);
if (store && store.info.cartEnabled) {
const cartBtnTitle = productElement.getAttribute('data-swb-product-add-cart-title');
const addToCartBtn = document.createElement('button');
addToCartBtn.textContent = cartBtnTitle || 'Add to Cart';
addToCartBtn.classList.add('swb-add-to-cart');
addToCartBtn.onclick = () => {
const product = {
sku: productElement.getAttribute('data-swb-product-sku'),
name: productElement.getAttribute('data-swb-product-name'),
price: parseFloat(productElement.getAttribute('data-swb-product-price')),
quantity: 1
};
this.addToCart(storeId, product);
};
buttonsContainer.appendChild(addToCartBtn);
}
}
handleSearch(query, storeId) {
const store = this.stores.get(storeId);
if (!store) return;
query = query.toLowerCase();
store.filteredProducts = store.products.filter(product =>
product.name.toLowerCase().includes(query) ||
product.sku.toLowerCase().includes(query)
);
this.updateProductDisplay(storeId);
}
handleSort(field, direction, storeId) {
const store = this.stores.get(storeId);
if (!store) return;
store.sortState.field = field;
store.sortState.direction = direction;
const sortedProducts = [...store.filteredProducts];
sortedProducts.sort((a, b) => {
let valueA, valueB;
if (field === 'price') {
valueA = a.price;
valueB = b.price;
} else {
valueA = a.name.toLowerCase();
valueB = b.name.toLowerCase();
}
if (field === 'price') {
return direction === 'asc' ? valueA - valueB : valueB - valueA;
} else {
return direction === 'asc' ?
valueA.localeCompare(valueB) :
valueB.localeCompare(valueA);
}
});
store.filteredProducts = sortedProducts;
this.updateProductDisplay(storeId);
}
updateProductDisplay(storeId) {
const store = this.stores.get(storeId);
if (!store) return;
const catalog = document.querySelector(`[data-swb-catalog][data-swb-store-id="${storeId}"]`);
if (!catalog) return;
const productGrid = catalog.querySelector('.swb-grid');
if (!productGrid) return;
Array.from(productGrid.children).forEach(child => {
productGrid.removeChild(child);
});
store.filteredProducts.forEach(product => {
productGrid.appendChild(product.element);
});
}
setupEventListeners() {
document.addEventListener('click', (e) => {
if (e.target.matches('.swb-modal-close') || e.target.closest('.swb-modal-close')) {
const modal = e.target.closest('.swb-cart-modal');
if (modal) {
const storeId = modal.getAttribute('data-swb-store-id');
this.closeCheckout(storeId);
}
}
});
}
updateCartCount(storeId) {
const store = this.stores.get(storeId);
if (!store) return;
const count = store.cart.reduce((total, item) => total + item.quantity, 0);
document.querySelectorAll(`[data-swb-store-id="${storeId}"] [data-swb-cart-count]`).forEach(counter => {
counter.textContent = count;
});
const modalCounters = document.querySelectorAll(`.swb-cart-modal[data-swb-store-id="${storeId}"] .swb-cart-count`);
modalCounters.forEach(counter => {
if (counter.closest('.swb-cart-subheader')) {
counter.textContent = store.cart.length;
} else {
counter.textContent = count;
}
});
}
addToCart(storeId, product) {
const store = this.stores.get(storeId);
if (!store) return;
const existingProduct = store.cart.find(item => item.sku === product.sku);
if (existingProduct) {
existingProduct.quantity++;
} else {
store.cart.push(product);
}
this.saveCartToStorage(storeId, store.cart);
this.updateCartCount(storeId);
this.renderCart(storeId);
}
renderCart(storeId) {
const cartModal = document.querySelector(`.swb-cart-modal[data-swb-store-id="${storeId}"]`);
if (!cartModal) return;
const store = this.stores.get(storeId);
if (!store) return;
const cartItems = cartModal.querySelector('.swb-cart-items');
cartItems.innerHTML = '';
store.cart.forEach(item => {
const itemElement = document.createElement('div');
itemElement.classList.add('swb-cart-item');
itemElement.innerHTML = `
<div class="swb-item-name">${item.name}</div>
<div class="swb-item-quantity">
<button onclick="swb.updateQuantity('${storeId}', '${item.sku}', -1)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="12" height="12" fill="currentColor">
<path d="M416 208H32c-17.7 0-32 14.3-32 32v32c0 17.7 14.3 32 32 32h384c17.7 0 32-14.3 32-32v-32c0-17.7-14.3-32-32-32z"/>
</svg>
</button>
<span>${item.quantity}</span>
<button onclick="swb.updateQuantity('${storeId}', '${item.sku}', 1)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="12" height="12" fill="currentColor">
<path d="M416 208H272V64c0-17.7-14.3-32-32-32h-32c-17.7 0-32 14.3-32 32v144H32c-17.7 0-32 14.3-32 32v32c0 17.7 14.3 32 32 32h144v144c0 17.7 14.3 32 32 32h32c17.7 0 32-14.3 32-32V304h144c17.7 0 32-14.3 32-32v-32c0-17.7-14.3-32-32-32z"/>
</svg>
</button>
</div>
<div class="swb-item-price">${this.formatPrice(item.price * item.quantity)}</div>
<button onclick="swb.removeFromCart('${storeId}', '${item.sku}')" class="swb-remove-item">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 352 512">
<path d="M242.7 256l100.1-100.1c12.3-12.3 12.3-32.2 0-44.5l-22.2-22.2c-12.3-12.3-32.2-12.3-44.5 0L176 189.3 75.9 89.2c-12.3-12.3-32.2-12.3-44.5 0L9.2 111.5c-12.3 12.3-12.3 32.2 0 44.5L109.3 256 9.2 356.1c-12.3 12.3-12.3 32.2 0 44.5l22.2 22.2c12.3 12.3 32.2 12.3 44.5 0L176 322.7l100.1 100.1c12.3 12.3 32.2 12.3 44.5 0l22.2-22.2c12.3-12.3 12.3-32.2 0-44.5L242.7 256z"/>
</svg>
</button>
`;
cartItems.appendChild(itemElement);
});
this.updateTotal(storeId);
}
updateQuantity(storeId, sku, change) {
const store = this.stores.get(storeId);
if (!store) return;
const product = store.cart.find(item => item.sku === sku);
if (product) {
product.quantity += change;
if (product.quantity <= 0) {
this.removeFromCart(storeId, sku);
} else {
this.saveCartToStorage(storeId, store.cart);
this.renderCart(storeId);
this.updateCartCount(storeId);
}
}
}
removeFromCart(storeId, sku) {
const store = this.stores.get(storeId);
if (!store) return;
store.cart = store.cart.filter(item => item.sku !== sku);
this.saveCartToStorage(storeId, store.cart);
this.renderCart(storeId);
this.updateCartCount(storeId);
}
updateTotal(storeId) {
const store = this.stores.get(storeId);
if (!store) return;
const total = store.cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const totalElement = document.querySelector(`.swb-cart-modal[data-swb-store-id="${storeId}"] .swb-cart-total`);
if (totalElement) {
totalElement.textContent = `Total: ${this.formatPrice(total)}`;
}
}
showCheckout(storeId) {
const store = this.stores.get(storeId);
if (!store || store.cart.length === 0) {
alert('Your cart is empty');
return;
}
let modal = document.querySelector(`.swb-cart-modal[data-swb-store-id="${storeId}"]`);
if (!modal) {
modal = document.createElement('div');
modal.classList.add('swb-cart-modal');
modal.setAttribute('data-swb-store-id', storeId);
const customFieldsHtml = store.info.customFields.map(field => `
<div class="swb-custom-field">
<input type="${field.type || 'text'}"
name="${field.name}"
placeholder="${field.placeholder || ''}"
${field.required ? 'required' : ''}
${field.pattern ? `pattern="${field.pattern}"` : ''}
${field.min ? `min="${field.min}"` : ''}
${field.max ? `max="${field.max}"` : ''}>
</div>
`).join('');
modal.innerHTML = `
<div class="swb-modal-content">
<div class="swb-cart-header">
<h2>${store.info.checkoutTitle}</h2>
</div>
<div class="swb-modal-close">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512" width="16" height="16" fill="currentColor">
<path d="M242.7 256l100.1-100.1c12.3-12.3 12.3-32.2 0-44.5l-22.2-22.2c-12.3-12.3-32.2-12.3-44.5 0L176 189.3 75.9 89.2c-12.3-12.3-32.2-12.3-44.5 0L9.2 111.5c-12.3 12.3-12.3 32.2 0 44.5L109.3 256 9.2 356.1c-12.3 12.3-12.3 32.2 0 44.5l22.2 22.2c12.3 12.3 32.2 12.3 44.5 0L176 322.7l100.1 100.1c12.3 12.3 32.2 12.3 44.5 0l22.2-22.2c12.3-12.3 12.3-32.2 0-44.5L242.7 256z"/>
</svg>
</div>
<div class="swb-cart-subheader">
<p>Items</p>
<button class="swb-clear-cart">Clear All</button>
</div>
<div class="swb-cart-items"></div>
<div class="swb-cart-total">Total: ${this.formatPrice(0)}</div>
<form class="swb-checkout-form">
${store.info.enableBilling ? `
<h2>${store.info.billingTitle}</h2>
<input type="text" name="name" placeholder="Full Name" required>
<input type="email" name="email" placeholder="Email" required>
<input type="tel" name="phone" placeholder="Phone Number" required>
<textarea name="address" placeholder="Delivery Address" required></textarea>
` : ''}
${customFieldsHtml}
<button type="submit">${store.info.submitButtonText}</button>
</form>
</div>
`;
document.body.appendChild(modal);
const form = modal.querySelector('.swb-checkout-form');
if (form) {
form.onsubmit = (e) => {
e.preventDefault();
this.processCheckout(storeId, new FormData(form));
};
}
const clearBtn = modal.querySelector('.swb-clear-cart');
clearBtn.onclick = () => {
if (confirm('Are you sure you want to clear your cart?')) {
this.clearCart(storeId);
}
};
}
this.renderCart(storeId);
this.updateCartCount(storeId);
modal.style.display = 'block';
}
closeCheckout(storeId) {
const modal = document.querySelector(`.swb-cart-modal[data-swb-store-id="${storeId}"]`);
if (modal) {
modal.style.display = 'none';
}
}
processCheckout(storeId, formData) {
const store = this.stores.get(storeId);
if (!store) return;
const customerInfo = {};
formData.forEach((value, key) => {
customerInfo[key] = value;
});
const message = this.formatWhatsAppMessage(storeId, customerInfo);
const whatsappUrl = `https://wa.me/${store.info.whatsapp}?text=${encodeURIComponent(message)}`;
window.open(whatsappUrl, '_blank');
this.clearCart(storeId);
}
formatWhatsAppMessage(storeId, customerInfo) {
const store = this.stores.get(storeId);
if (!store) return '';
const orderDate = new Date().toLocaleString();
let message = `New Order from ${store.info.name}\n`;
message += `Date: ${orderDate}\n\n`;
if (store.info.enableBilling) {
message += `Customer Information:\n`;
message += `Name: ${customerInfo.name}\n`;
message += `Email: ${customerInfo.email}\n`;
message += `Phone: ${customerInfo.phone}\n`;
message += `Address: ${customerInfo.address}\n\n`;
}
store.info.customFields.forEach(field => {
if (customerInfo[field.name]) {
message += `${field.label || field.name}: ${customerInfo[field.name]}\n`;
}
});
message += `Order Details:\n`;
store.cart.forEach(item => {
message += `- ${item.name} (SKU: ${item.sku})\n`;
message += ` Quantity: ${item.quantity}\n`;
message += ` Price: ${this.formatPrice(item.price * item.quantity)}\n\n`;
});
const total = store.cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
message += `Total Amount: ${this.formatPrice(total)}`;
return message;
}
}
window.swb = new SWB();