UNPKG

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
// 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();