ondc-campaign-sdk
Version:
[](https://www.npmjs.com/package/ondc-campaign-sdk) [](LICENSE) [ • 23.2 kB
text/typescript
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;
}