UNPKG

ondc-campaign-sdk

Version:

[![npm version](https://img.shields.io/npm/v/ondc-campaign-sdk.svg)](https://www.npmjs.com/package/ondc-campaign-sdk) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Made with ❤️](https://img.shields.io/badge/Made%20with-%

763 lines (655 loc) 23.2 kB
import { fetchLiveCampaignProducts } from "./api"; export type StyleConfig = Partial<{ primary: string; primaryDark: string; accent: string; text: string; textLight: string; bgLight: string; white: string; shadow: string; borderRadius: string; }>; export async function fetchLiveCampaignProductsWithHtml( productCount: number = 10, style: StyleConfig = {} ): Promise<string> { // Try to use transactions in browser environments only const withTransaction = typeof window !== 'undefined'; const result = await fetchLiveCampaignProducts(withTransaction); // Handle both response formats (with or without transaction) const campaign = result?.campaign || result; const transaction = result?.transaction; const userId = result?.user_id; const noCampaignContentFound = `<div style="text-align: center; padding: 40px;"> <img src="https://cdn-icons-png.flaticon.com/512/4076/4076549.png" alt="No Campaign" width="120" style="opacity: 0.6;" /> <h2 style="color: #555; margin-top: 20px;">No Active Campaign Found</h2> <p style="color: #888;">Please check back later or contact your administrator.</p> </div> `; if(!campaign || campaign.message) { return noCampaignContentFound; } const defaultStyle: Required<StyleConfig> = { primary: "#3d5af1", primaryDark: "#2a3eb1", accent: "#ff6b6b", text: "#333333", textLight: "#777777", bgLight: "#f8f9fa", white: "#ffffff", shadow: "0 10px 30px rgba(0,0,0,0.08)", borderRadius: "12px", }; const mergedStyle = { ...defaultStyle, ...style }; const liveProductCount = campaign?.products?.length || 0; if (!liveProductCount || !campaign) { return noCampaignContentFound; } const productCards = campaign.products .slice(0, productCount) .map((product: any, index: number) => { // --- Data Normalization & Backward Compatibility --- // Ensure product has an ID if (!product.productId && product.id) { product.productId = product.id.toString(); } // Ensure product has a name if (!product.productName && product.name) { product.productName = product.name; } // Make sure we have the image URL if (product.imgUrl && product.imgUrl.startsWith('/')) { product.imgUrl = `https://cdnaz.plotch.io/image/upload/w_300,h_450${product.imgUrl}?product_id=${product.productId}&s=1&tf=vt`; } // For backward compatibility with older schema if (product.base_image && product.base_image.original_image_url && !product.imgUrl) { product.imgUrl = product.base_image.original_image_url; } // Normalize prices for consistency if (typeof product.regularPrice === 'undefined' && product.prices?.regular?.price) { product.regularPrice = parseFloat(product.prices.regular.price); } if (typeof product.discountedPrice === 'undefined' && product.prices?.final?.price) { product.discountedPrice = parseFloat(product.prices.final.price); } // Calculate discount percentage if not provided if (typeof product.discountPercentage === 'undefined' && product.regularPrice && product.discountedPrice && product.regularPrice > product.discountedPrice) { const discount = product.regularPrice - product.discountedPrice; product.discountPercentage = Math.round((discount / product.regularPrice) * 100); } // Normalize ratings if (typeof product.productRatings === 'undefined' && product.ratings?.average) { product.productRatings = parseFloat(product.ratings.average); } // Build product URL through redirect API const productName = product.productName .toLowerCase() .replace(/[^a-z0-9\s-]/g, '') // Remove special chars except hyphen .replace(/\s+/g, '-') // Replace spaces with single hyphen .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen .replace(/(\d+)\s*(kg|g|ml|l)\b/i, '$1$2'); // Join number and unit without space const productUrl = `https://shop.samhita.org/product/${productName}/${product.productId}?affiliate_id=${transaction?.data?.affiliate_id}&user_id=${userId}`; // Calculate discount percentage or use provided value const discountPercent = product.discountPercentage || (product.regularPrice && product.discountedPrice ? Math.round((1 - product.discountedPrice / product.regularPrice) * 100) : 0); // Format prices with currency const formattedRegularPrice = `₹${product.regularPrice?.toLocaleString('en-IN') || ''}`; const formattedDiscountedPrice = `₹${product.discountedPrice?.toLocaleString('en-IN') || ''}`; // Get primary image const productImage = product.imgUrl || (product.galleryImages && product.galleryImages.length > 0 ? product.galleryImages[0].url : ''); // Prepare rating stars const rating = product.productRatings || 0; const ratingStars = Array(5).fill('') .map((_, i) => i < rating ? '★' : '☆') .join(''); // Get brand and vendor information const brandName = product.brandName || ''; const vendorName = product.vendorName || ''; // Get delivery, category and other details const estimatedDelivery = product.estimatedDeliveryTime || ''; const categoryDisplay = product.categoryName && product.categoryName.length > 0 ? product.categoryName.join(', ') : ''; const unitDisplay = product.unit ? `Per ${product.unit}` : ''; // Shipping and returns info const returnable = product.returnable === 'Yes' ? 'Returnable' : (product.returnable === 'No' ? 'Non-returnable' : ''); const cancelable = product.cancellable === 'Yes' ? 'Can be canceled' : ''; // Animation delay for staggered entrance const animationDelay = (index * 0.1).toFixed(1); return ` <div class="product-card" style="animation-delay: ${animationDelay}s;"> ${discountPercent > 0 ? `<div class="discount-tag"> <span class="discount-value">-${discountPercent}%</span> </div>` : ''} <div class="card-top"> <div class="product-image-container"> <img src="${productImage}" alt="${product.productName || ''}" loading="lazy" onerror="this.onerror=null;this.src='https://www.howmet.com/wp-content/themes/howmet/assets/build/images/placeholder-image.db2b4d5c.jpeg';" /> ${brandName ? `<div class="brand-flag">${brandName}</div>` : ''} </div> </div> <div class="card-content"> <h3 class="product-title">${product.productName || ''}</h3> <div class="info-section"> ${categoryDisplay ? `<div class="product-category">${categoryDisplay}</div>` : ''} ${rating > 0 ? `<div class="rating-row"> <div class="stars">${ratingStars}</div> <span class="rating-number">${rating.toFixed(1)}</span> </div>` : ''} </div> <div class="price-row"> ${product.discountedPrice && product.regularPrice ? `<div class="price-current">${formattedDiscountedPrice}</div> <div class="price-original">${formattedRegularPrice}</div>` : `<div class="price-current price-only">${formattedRegularPrice}</div>` } ${unitDisplay ? `<div class="unit-display">${unitDisplay}</div>` : ''} </div> ${vendorName ? `<div class="vendor-badge">Sold by ${vendorName}</div>` : ''} ${estimatedDelivery ? `<div class="delivery-info"> <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> <span>${estimatedDelivery}</span> </div>` : ''} ${returnable || cancelable ? `<div class="shipping-info"> ${returnable ? `<span class="shipping-tag">${returnable}</span>` : ''} ${cancelable ? `<span class="shipping-tag">${cancelable}</span>` : ''} </div>` : ''} <div class="cta-container"> <a href="${productUrl}" class="view-btn" target="_blank" aria-label="View ${product.productName || 'product'}" onclick=" (function(event) { event.preventDefault(); // Define all variables needed inline var campaignId = '${campaign._id}'; var productId = '${product.productId}'; var affiliateId = '${transaction?.data?.affiliate_id || ''}'; var targetUrl = '${productUrl}'; var userId = '${userId || ''}'; // Create the API request fetch('https://ondc-sdk.samhita.org/api/transactions/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ campaign_id: campaignId, product_id: productId, affiliate_id: affiliateId || undefined, status: 'initiated', user_id: userId }) }) .then(function(response) { if (response.ok) { window.open(targetUrl, '_blank'); } else { console.error('Failed to update transaction'); } }) .catch(function(err) { console.error('Error updating transaction:', err); }); return false; })(event); " > <span>View Details</span> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M5 12h14"></path> <path d="M12 5l7 7-7 7"></path> </svg> </a> </div> </div> </div> `; }) .join(""); const campaignContent = ` <style> :root { --primary: ${mergedStyle.primary}; --primary-dark: ${mergedStyle.primaryDark}; --accent: ${mergedStyle.accent}; --text: ${mergedStyle.text}; --text-light: ${mergedStyle.textLight}; --bg-light: ${mergedStyle.bgLight}; --white: ${mergedStyle.white}; --shadow: ${mergedStyle.shadow}; --border-radius: ${mergedStyle.borderRadius}; --card-radius: 16px; --ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1); } /* Base styles */ .ondc-campaign { font-family: 'Inter', system-ui, -apple-system, sans-serif; width: 100%; max-width: 1400px; margin: 0 auto; padding: 2rem; background: var(--bg-light); border-radius: 24px; overflow: hidden; box-sizing: border-box; } .ondc-campaign *, .ondc-campaign *:before, .ondc-campaign *:after { box-sizing: border-box; } /* Campaign header */ .campaign-banner { width: 100%; border-radius: 18px; overflow: hidden; margin-bottom: 2rem; box-shadow: 0 15px 30px rgba(0,0,0,0.06); position: relative; } .campaign-banner img { width: 100%; height: 100%; object-fit: cover; display: block; max-height: fit-content; } .campaign-header-content { margin-bottom: 3rem; text-align: center; position: relative; padding: 0 1rem; } .title-accent { display: inline-block; font-size: 0.9rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--primary); background-color: rgba(61, 90, 241, 0.08); padding: 6px 16px; border-radius: 100px; margin-bottom: 1rem; } .campaign-header-content::after { content: ''; position: absolute; bottom: -1.5rem; left: 50%; transform: translateX(-50%); width: 80px; height: 3px; background: var(--primary); border-radius: 3px; } .campaign-title { font-size: 2.8rem; font-weight: 800; color: var(--text); margin: 0 0 1.2rem; line-height: 1.2; letter-spacing: -0.02em; background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; text-fill-color: transparent; } .campaign-description { font-size: 1.2rem; color: var(--text-light); line-height: 1.8; margin: 0 auto; max-width: 800px; font-weight: 400; } /* Products grid */ .products-section { padding: 0; } .products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 2rem; position: relative; } /* Product card */ .product-card { background: var(--white); border-radius: var(--card-radius); overflow: hidden; position: relative; transition: transform 0.4s var(--ease-out-expo), box-shadow 0.4s var(--ease-out-expo); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05); height: 100%; display: flex; flex-direction: column; transform: translateY(20px); opacity: 0; animation: fadeInUp 0.7s var(--ease-out-expo) forwards; } @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .product-card:hover { transform: translateY(-10px); box-shadow: 0 20px 40px rgba(0, 0, 0, 0.08), 0 10px 20px rgba(0, 0, 0, 0.05); } .product-card:hover .product-image-container img { transform: scale(1.08); } .discount-tag { position: absolute; top: 16px; right: 16px; z-index: 5; background: var(--accent); color: white; border-radius: 100px; height: 44px; width: 44px; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.75rem; box-shadow: 0 4px 10px rgba(255, 107, 107, 0.3); } .discount-value { line-height: 1; display: block; } .card-top { position: relative; overflow: hidden; border-radius: var(--card-radius) var(--card-radius) 0 0; height: 240px; } .product-image-container { width: 100%; height: 100%; background: #f5f5f5; position: relative; overflow: hidden; } .product-image-container img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.5s var(--ease-out-expo); } .brand-flag { position: absolute; bottom: 0; left: 0; background: rgba(0,0,0,0.7); color: white; padding: 6px 12px; font-size: 0.75rem; font-weight: 600; border-top-right-radius: 8px; text-transform: uppercase; letter-spacing: 0.5px; } .card-content { padding: 1.5rem; display: flex; flex-direction: column; flex-grow: 1; position: relative; } .product-title { font-size: 1.15rem; font-weight: 600; line-height: 1.4; color: var(--text); margin: 0 0 12px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .info-section { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; } .product-category { font-size: 0.8rem; color: var(--text-light); background: rgba(0,0,0,0.05); padding: 4px 10px; border-radius: 100px; display: inline-block; } .price-row { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; position: relative; } .price-current { font-size: 1.5rem; font-weight: 700; color: var(--primary); } .price-original { font-size: 0.9rem; text-decoration: line-through; color: var(--text-light); font-weight: 400; } .price-only { color: #2e2e2e; } .unit-display { position: absolute; right: 0; bottom: -12px; font-size: 0.75rem; color: var(--text-light); } .rating-row { display: flex; align-items: center; gap: 6px; } .stars { color: #FFB800; font-size: 0.9rem; letter-spacing: 1px; } .rating-number { color: var(--text-light); font-size: 0.8rem; font-weight: 500; } .vendor-badge { display: inline-block; font-size: 0.85rem; color: var(--text); margin-bottom: 1rem; font-weight: 500; } .delivery-info { display: flex; align-items: center; gap: 6px; color: var(--text-light); font-size: 0.85rem; margin-bottom: 10px; } .delivery-info svg { color: var(--primary); } .shipping-info { display: flex; gap: 10px; margin-bottom: 1.2rem; flex-wrap: wrap; } .shipping-tag { display: inline-block; font-size: 0.75rem; color: var(--text-light); background: rgba(0,0,0,0.05); padding: 3px 8px; border-radius: 4px; } .cta-container { margin-top: auto; } .view-btn { display: flex; align-items: center; justify-content: center; gap: 8px; background-color: var(--primary); color: white; border: none; padding: 12px 24px; border-radius: 8px; font-weight: 600; font-size: 0.95rem; text-decoration: none; width: 100%; transition: all 0.3s ease; box-shadow: 0 5px 15px rgba(61, 90, 241, 0.2); } .view-btn svg { transition: transform 0.3s ease; } .view-btn:hover { background-color: var(--primary-dark); box-shadow: 0 8px 20px rgba(61, 90, 241, 0.3); } .view-btn:hover svg { transform: translateX(3px); } /* Responsive styles */ @media (max-width: 1200px) { .campaign-title { font-size: 2.5rem; } .campaign-banner { height: 240px; } } @media (max-width: 992px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 1.5rem; } .campaign-title { font-size: 2.2rem; } .campaign-description { font-size: 1.1rem; line-height: 1.7; } .campaign-header-content::after { width: 70px; height: 3px; bottom: -1.2rem; } } @media (max-width: 768px) { .ondc-campaign { padding: 1.5rem; } .campaign-banner { height: 200px; margin-bottom: 1.5rem; } .title-accent { font-size: 0.8rem; padding: 5px 14px; margin-bottom: 0.8rem; } .campaign-title { font-size: 2rem; } .campaign-description { font-size: 1rem; line-height: 1.6; } .campaign-header-content { margin-bottom: 2.5rem; } .campaign-header-content::after { width: 60px; height: 2.5px; bottom: -1rem; } .card-top { height: 200px; } } @media (max-width: 576px) { .ondc-campaign { padding: 1rem; } .campaign-banner { height: 160px; } .campaign-title { font-size: 1.5rem; } .products-grid { grid-template-columns: 1fr; gap: 1.2rem; } .product-title { font-size: 1.1rem; } .price-current { font-size: 1.3rem; } .view-btn { padding: 10px 20px; font-size: 0.9rem; } } </style> <div class="ondc-campaign"> <div class="campaign-banner"> <img src="${campaign.banner}" alt="${campaign.campaignName || ''}" onerror="this.onerror=null;this.src='https://via.placeholder.com/1200x400?text=Campaign';" /> </div> <div class="campaign-header-content"> ${campaign.campaignName ? '<span class="title-accent">Featured Campaign</span>' : ''} <h1 class="campaign-title">${campaign.campaignName || 'Featured Products'}</h1> <p class="campaign-description">${campaign.description || 'Browse our collection of curated products'}</p> </div> <div class="products-section"> <div class="products-grid"> ${productCards} </div> </div> </div> `; return liveProductCount ? campaignContent : noCampaignContentFound; } export async function renderLiveCampaignProducts( productCount: number = 10, style: StyleConfig = {} ): Promise<void> { const html = await fetchLiveCampaignProductsWithHtml(productCount, style); document.body.innerHTML = html; }