senangwebs-buy
Version:
Lightweight JavaScript library that that enables easy implementation of e-commerce functionality through HTML attributes
749 lines (652 loc) • 24.3 kB
JavaScript
// SenangWebs Buy Library
const icons = require('@bookklik/senangstart-icons/icons');
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 = `
${icons['shopping-cart']}
<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)">
${icons['minus']}
</button>
<span>${item.quantity}</span>
<button onclick="swb.updateQuantity('${storeId}', '${
item.sku
}', 1)">
${icons['plus']}
</button>
</div>
<div class="swb-item-price">${
item.price ? this.formatPrice(item.price * item.quantity) : ""
}</div>
<button onclick="swb.removeFromCart('${storeId}', '${
item.sku
}')" class="swb-remove-item">
${icons['x-mark']}
</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.updateTotal(storeId);
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.price * item.quantity : 0),
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">
${icons['x-mark']}
</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`;
if (item.price) {
message += ` Price: ${this.formatPrice(
item.price * item.quantity
)}\n\n`;
}
});
const total = store.cart.reduce(
(sum, item) => (sum + item.price ? item.price * item.quantity : 0),
0
);
if (total > 0) {
message += `Total Amount: ${this.formatPrice(total)}`;
}
return message;
}
}
window.swb = new SWB();