optumflex-subscription-ui
Version:
A comprehensive React UI component library for subscription management, pricing tables, shopping cart, and checkout systems with full customization support
719 lines (688 loc) • 38.2 kB
JavaScript
import React, { useState, useCallback, useEffect, useMemo, forwardRef, createElement } from 'react';
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
// Removed constants import - using inline values
// Inline constants
const BILLING_CYCLES = ["weekly", "monthly", "quarterly", "halfyearly", "yearly"];
const DEFAULT_BILLING_CYCLE = 'monthly';
const CURRENCY_SYMBOL = '₹';
const DISCOUNT_THRESHOLD = 0;
/**
* Parse features from API response format
*/
const parseFeatures = (featuresArr) => {
return featuresArr
.map((f) => Object.values(f)[0])
.filter((v) => typeof v === 'string' && v.trim() !== '');
};
/**
* Parse a single plan from API response
*/
const parsePlan = (pkg) => ({
id: pkg.package_code_str,
title: pkg.package_name,
description: pkg.description,
prices: pkg.prices,
discounted: pkg.discounted,
features: parseFeatures(pkg.features),
});
/**
* Find the minimum price cycle for a plan
*/
const getMinimumPriceCycle = (prices) => {
const validPrices = Object.entries(prices)
.filter(([_, price]) => price > 0)
.sort(([_, a], [__, b]) => a - b);
return validPrices.length > 0 ? validPrices[0][0] : DEFAULT_BILLING_CYCLE;
};
/**
* Get available billing cycles for a plan
*/
const getAvailableCycles = (prices) => {
return BILLING_CYCLES.filter((cycle) => prices[cycle] > 0);
};
/**
* Calculate discount information for a plan and cycle
*/
const calculateDiscountInfo = (plan, cycle) => {
const originalPrice = plan.prices[cycle];
const discountedPrice = plan.discounted[cycle];
const isDiscounted = discountedPrice > 0 && discountedPrice < originalPrice;
const savings = isDiscounted ? originalPrice - discountedPrice : 0;
const savingsPercentage = isDiscounted && originalPrice > 0 ? Math.round((savings / originalPrice) * 100) : 0;
return {
originalPrice,
discountedPrice: isDiscounted ? discountedPrice : originalPrice,
savings,
savingsPercentage,
isDiscounted,
};
};
/**
* Sort plans based on criteria
*/
const sortPlans = (plans, sortBy, selectedCycles) => {
return [...plans].sort((a, b) => {
if (sortBy === 'price-asc') {
const aCycle = selectedCycles[a.id] || DEFAULT_BILLING_CYCLE;
const bCycle = selectedCycles[b.id] || DEFAULT_BILLING_CYCLE;
const aPrice = a.discounted[aCycle] || a.prices[aCycle];
const bPrice = b.discounted[bCycle] || b.prices[bCycle];
return aPrice - bPrice;
}
else if (sortBy === 'price-desc') {
const aCycle = selectedCycles[a.id] || DEFAULT_BILLING_CYCLE;
const bCycle = selectedCycles[b.id] || DEFAULT_BILLING_CYCLE;
const aPrice = a.discounted[aCycle] || a.prices[aCycle];
const bPrice = b.discounted[bCycle] || b.prices[bCycle];
return bPrice - aPrice;
}
else if (sortBy === 'name') {
const aName = a.title || a.package_name || '';
const bName = b.title || b.package_name || '';
return aName.localeCompare(bName);
}
return 0;
});
};
/**
* Generate checkout URL with cart data
*/
const generateCheckoutUrl = (cart, baseUrl) => {
const base64 = typeof window !== 'undefined'
? btoa(unescape(encodeURIComponent(JSON.stringify(cart))))
: '';
return `${baseUrl}/checkout?cart=${encodeURIComponent(base64)}`;
};
/**
* Handle buy now functionality for a single plan
*/
const handleBuyNow = (plan, cycle, companyInfo) => {
if (!companyInfo?.application) {
console.error('Company application URL not configured');
return;
}
const payload = [{
packageCode: plan.id,
duration: cycle,
}];
const base64 = typeof window !== 'undefined'
? btoa(unescape(encodeURIComponent(JSON.stringify(payload))))
: '';
const checkoutUrl = `${companyInfo.application}/checkout?cart=${encodeURIComponent(base64)}`;
window.open(checkoutUrl, '_blank');
};
/**
* Format price with currency symbol
*/
const formatPrice = (price) => {
return `${CURRENCY_SYMBOL}${price}`;
};
/**
* Calculate total cart value
*/
const calculateCartTotal = (cart) => {
return cart.reduce((total, item) => {
const discountInfo = calculateDiscountInfo(item.plan, item.cycle);
return total + discountInfo.discountedPrice;
}, 0);
};
/**
* Create checkout payload from cart items
*/
const createCheckoutPayload = (cart) => {
return cart.map(item => ({
packageCode: item.plan.id,
duration: item.cycle,
}));
};
/**
* Check if a plan has any discounts
*/
const hasDiscounts = (plan) => {
return BILLING_CYCLES.some(cycle => {
const discountInfo = calculateDiscountInfo(plan, cycle);
return discountInfo.isDiscounted && discountInfo.savings > DISCOUNT_THRESHOLD;
});
};
/**
* Get the best discount percentage for a plan
*/
const getBestDiscountPercentage = (plan) => {
let bestDiscount = 0;
BILLING_CYCLES.forEach(cycle => {
const discountInfo = calculateDiscountInfo(plan, cycle);
if (discountInfo.isDiscounted && discountInfo.originalPrice > 0) {
const percentage = (discountInfo.savings / discountInfo.originalPrice) * 100;
bestDiscount = Math.max(bestDiscount, percentage);
}
});
return Math.round(bestDiscount);
};
/**
* Validate plan data
*/
const validatePlan = (plan) => {
return !!(plan?.package_code_str &&
plan?.package_name &&
plan?.prices &&
plan?.discounted &&
Array.isArray(plan?.features));
};
/**
* Filter plans by search query
*/
const filterPlansBySearch = (plans, searchQuery) => {
if (!searchQuery.trim())
return plans;
const query = searchQuery.toLowerCase();
return plans.filter(plan => plan.title.toLowerCase().includes(query) ||
plan.description.toLowerCase().includes(query) ||
plan.features.some(feature => feature.toLowerCase().includes(query)));
};
// Removed API import - using provided data
const useSubscription = (options = {}) => {
const { initialPlanType = 'subscriptionPlans', initialSortBy = 'default', initialData, config = {}, onError, onSuccess } = options;
// State
const [subscriptionPlans, setSubscriptionPlans] = useState([]);
const [modelPortfolios, setModelPortfolios] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [selectedCycles, setSelectedCycles] = useState({});
const [sortBy, setSortBy] = useState(initialSortBy);
const [planType, setPlanType] = useState(initialPlanType);
// Helper function to find the minimum price cycle
const getMinimumPriceCycle = useCallback((prices) => {
const validPrices = Object.entries(prices)
.filter(([_, price]) => price > 0)
.sort(([_, a], [__, b]) => a - b);
return validPrices.length > 0 ? validPrices[0][0] : 'monthly';
}, []);
// Fetch data
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
let data;
if (initialData) {
// Use provided data instead of fetching
data = initialData;
}
else {
// Use provided data only - no API fetching
throw new Error('No initial data provided');
}
setSubscriptionPlans(data.subscriptionPlans || []);
setModelPortfolios(data.modelPortfolios || []);
// Set default selected cycle for each plan based on minimum price
const defaultCycles = {};
[...(data.subscriptionPlans || []), ...(data.modelPortfolios || [])].forEach(plan => {
defaultCycles[plan.id] = getMinimumPriceCycle(plan.prices);
});
setSelectedCycles(prevCycles => ({
...defaultCycles,
...prevCycles // Preserve any existing selections
}));
onSuccess?.('Data loaded successfully');
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch data';
setError(errorMessage);
onError?.(errorMessage);
}
finally {
setLoading(false);
}
}, [onSuccess, onError, initialData, getMinimumPriceCycle]);
// Load data on mount
useEffect(() => {
fetchData();
}, [fetchData]);
// Computed values
const currentPlans = useMemo(() => {
return planType === 'subscriptionPlans' ? subscriptionPlans : modelPortfolios;
}, [planType, subscriptionPlans, modelPortfolios]);
const sortedPlans = useMemo(() => {
return sortPlans(currentPlans, sortBy, selectedCycles);
}, [currentPlans, sortBy, selectedCycles]);
const displayedPlans = useMemo(() => {
return sortedPlans;
}, [sortedPlans]);
// Actions
const setSelectedCycle = useCallback((planId, cycle) => {
setSelectedCycles(prev => ({ ...prev, [planId]: cycle }));
}, []);
const refreshData = useCallback(() => {
return fetchData();
}, [fetchData]);
const getFilteredPlans = useCallback((searchQuery) => {
return filterPlansBySearch(currentPlans, searchQuery || '');
}, [currentPlans]);
const getSortedPlans = useCallback(() => {
return sortedPlans;
}, [sortedPlans]);
const getDisplayedPlans = useCallback(() => {
return displayedPlans;
}, [displayedPlans]);
const getDiscountInfo = useCallback((plan, cycle) => {
return calculateDiscountInfo(plan, cycle);
}, []);
const getAvailableCyclesForPlan = useCallback((plan) => {
return getAvailableCycles(plan.prices);
}, []);
return {
// State
subscriptionPlans,
modelPortfolios,
loading,
error,
selectedCycles,
sortBy,
planType,
// State setters
setPlanType,
setSortBy,
setSelectedCycle,
// Data operations
refreshData,
getDisplayedPlans,
getSortedPlans,
getFilteredPlans,
// Utility functions
getDiscountInfo,
getAvailableCycles: getAvailableCyclesForPlan,
};
};
/**
* @license lucide-react v0.539.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
const toCamelCase = (string) => string.replace(
/^([A-Z])|[\s-_]+(\w)/g,
(match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase()
);
const toPascalCase = (string) => {
const camelCase = toCamelCase(string);
return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
};
const mergeClasses = (...classes) => classes.filter((className, index, array) => {
return Boolean(className) && className.trim() !== "" && array.indexOf(className) === index;
}).join(" ").trim();
const hasA11yProp = (props) => {
for (const prop in props) {
if (prop.startsWith("aria-") || prop === "role" || prop === "title") {
return true;
}
}
};
/**
* @license lucide-react v0.539.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
var defaultAttributes = {
xmlns: "http://www.w3.org/2000/svg",
width: 24,
height: 24,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: 2,
strokeLinecap: "round",
strokeLinejoin: "round"
};
/**
* @license lucide-react v0.539.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const Icon = forwardRef(
({
color = "currentColor",
size = 24,
strokeWidth = 2,
absoluteStrokeWidth,
className = "",
children,
iconNode,
...rest
}, ref) => createElement(
"svg",
{
ref,
...defaultAttributes,
width: size,
height: size,
stroke: color,
strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth,
className: mergeClasses("lucide", className),
...!children && !hasA11yProp(rest) && { "aria-hidden": "true" },
...rest
},
[
...iconNode.map(([tag, attrs]) => createElement(tag, attrs)),
...Array.isArray(children) ? children : [children]
]
)
);
/**
* @license lucide-react v0.539.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const createLucideIcon = (iconName, iconNode) => {
const Component = forwardRef(
({ className, ...props }, ref) => createElement(Icon, {
ref,
iconNode,
className: mergeClasses(
`lucide-${toKebabCase(toPascalCase(iconName))}`,
`lucide-${iconName}`,
className
),
...props
})
);
Component.displayName = toPascalCase(iconName);
return Component;
};
/**
* @license lucide-react v0.539.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const __iconNode$4 = [["path", { d: "M20 6 9 17l-5-5", key: "1gmf2c" }]];
const Check = createLucideIcon("check", __iconNode$4);
/**
* @license lucide-react v0.539.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const __iconNode$3 = [["path", { d: "M21 12a9 9 0 1 1-6.219-8.56", key: "13zald" }]];
const LoaderCircle = createLucideIcon("loader-circle", __iconNode$3);
/**
* @license lucide-react v0.539.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const __iconNode$2 = [
["circle", { cx: "8", cy: "21", r: "1", key: "jimo8o" }],
["circle", { cx: "19", cy: "21", r: "1", key: "13723u" }],
[
"path",
{
d: "M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12",
key: "9zh506"
}
]
];
const ShoppingCart = createLucideIcon("shopping-cart", __iconNode$2);
/**
* @license lucide-react v0.539.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const __iconNode$1 = [
["path", { d: "M10 11v6", key: "nco0om" }],
["path", { d: "M14 11v6", key: "outv1u" }],
["path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6", key: "miytrc" }],
["path", { d: "M3 6h18", key: "d0wm0j" }],
["path", { d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2", key: "e791ji" }]
];
const Trash2 = createLucideIcon("trash-2", __iconNode$1);
/**
* @license lucide-react v0.539.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const __iconNode = [
["path", { d: "M18 6 6 18", key: "1bl5f8" }],
["path", { d: "m6 6 12 12", key: "d8bk6v" }]
];
const X = createLucideIcon("x", __iconNode);
// Function to convert billing cycle to titlecase
const formatBillingCycle = (cycle) => {
return cycle.charAt(0).toUpperCase() + cycle.slice(1);
};
const Cart = ({ className = '', cart, isOpen, onClose, onRemoveItem, applicationUrl, colorScheme = {} }) => {
const cartTotal = calculateCartTotal(cart);
// Helper function to get inline styles for specific elements
const getInlineStyles = (type) => {
switch (type) {
case 'primary-text':
return colorScheme.primary ? { color: colorScheme.primary } : {};
case 'gradient-bg':
return colorScheme.gradient?.from && colorScheme.gradient?.to
? { background: `linear-gradient(to right, ${colorScheme.gradient.from}, ${colorScheme.gradient.to})` }
: {};
default:
return {};
}
};
const handleCheckout = () => {
if (!applicationUrl) {
console.error('Application URL not configured');
return;
}
const payload = cart.map(item => ({
packageCode: item.plan.id,
duration: item.cycle,
}));
const base64 = typeof window !== 'undefined'
? btoa(unescape(encodeURIComponent(JSON.stringify(payload))))
: '';
const checkoutUrl = `${applicationUrl}/checkout?cart=${encodeURIComponent(base64)}`;
window.open(checkoutUrl, '_blank');
};
return (jsx(Fragment, { children: jsxs("div", { className: `fixed inset-0 z-50 overflow-hidden transition-opacity duration-300 ${isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}`, children: [jsx("div", { className: `absolute inset-0 bg-black transition-opacity duration-300 ${isOpen ? 'bg-opacity-50' : 'bg-opacity-0'}`, onClick: onClose }), jsx("div", { className: `absolute right-0 top-0 h-full w-full max-w-md bg-white shadow-2xl transform transition-transform duration-300 ease-in-out ${isOpen ? 'translate-x-0' : 'translate-x-full'}`, children: jsxs("div", { className: "flex flex-col h-full", children: [jsxs("div", { className: "flex items-center justify-between p-6 border-b", style: { borderColor: colorScheme.border || '#E5E7EB' }, children: [jsx("h3", { className: "text-lg font-semibold", style: { color: colorScheme.text || '#1F2937' }, children: "Shopping Cart" }), jsx("button", { onClick: onClose, className: "transition-colors", style: { color: colorScheme.primary || '#6B7280' }, children: jsx(X, { className: "w-6 h-6" }) })] }), jsx("div", { className: "flex-1 overflow-y-auto p-6", children: cart.length === 0 ? (jsxs("div", { className: "text-center py-8", children: [jsx(ShoppingCart, { className: "w-16 h-16 mx-auto mb-4", style: { color: colorScheme.primary || '#D1D5DB' } }), jsx("p", { style: { color: colorScheme.text || '#6B7280' }, children: "Your cart is empty" })] })) : (jsx("div", { className: "space-y-4", children: cart.map((item) => {
const { originalPrice, discountedPrice, savings, isDiscounted } = calculateDiscountInfo(item.plan, item.cycle);
return (jsxs("div", { className: "flex items-center justify-between p-4 border rounded-lg", style: { borderColor: colorScheme.border || '#E5E7EB' }, children: [jsxs("div", { className: "flex-1", children: [jsx("h4", { className: "font-medium", style: { color: colorScheme.text || '#1F2937' }, children: item.plan.package_name || item.plan.title }), jsxs("p", { className: "text-sm", style: { color: colorScheme.primary || '#3B82F6' }, children: [formatBillingCycle(item.cycle), " - ", formatPrice(discountedPrice)] }), isDiscounted && (jsxs("div", { className: "text-xs mt-1", children: [jsx("del", { style: { color: '#9CA3AF' }, children: formatPrice(originalPrice) }), jsxs("span", { style: { color: '#16A34A', marginLeft: 8 }, children: ["Save ", formatPrice(savings)] })] }))] }), jsx("button", { onClick: () => onRemoveItem(item.plan.id), className: "text-red-500 hover:text-red-700 transition-colors", children: jsx(Trash2, { className: "w-5 h-5" }) })] }, `${item.plan.id}-${item.cycle}`));
}) })) }), cart.length > 0 && (jsxs("div", { className: "border-t p-6", style: { borderColor: colorScheme.border || '#E5E7EB' }, children: [jsxs("div", { className: "flex items-center justify-between mb-4", children: [jsx("span", { className: "text-lg font-semibold", style: { color: colorScheme.text || '#1F2937' }, children: "Total:" }), jsxs("span", { className: "text-lg font-bold", style: getInlineStyles('primary-text'), children: ["\u20B9", cartTotal] })] }), jsx("button", { onClick: handleCheckout, className: "w-full text-white py-3 px-4 rounded-lg font-semibold transition-all transform hover:scale-[1.02]", style: getInlineStyles('gradient-bg'), children: "Proceed to Checkout" })] }))] }) })] }) }));
};
const Subscription = ({ apiData, config = {}, applicationUrl, className = '', colorScheme = {}, renderPlanCard, renderHeader, renderCart }) => {
const { loading, error, selectedCycles, sortBy, planType, setPlanType, setSortBy, setSelectedCycle, getDisplayedPlans, getDiscountInfo, getAvailableCycles, } = useSubscription({
initialData: apiData,
config
});
const displayedPlans = getDisplayedPlans();
// Plain state management instead of Context
const [cart, setCart] = useState([]);
const [isCartOpen, setIsCartOpen] = useState(false);
// Cart functions
const addToCart = (plan, cycle) => {
const normalizedId = resolvePlanId(plan);
const normalizedPlan = { ...plan, id: normalizedId };
setCart(prev => {
const existingIndex = prev.findIndex(item => resolvePlanId(item.plan) === normalizedId);
if (existingIndex >= 0) {
// Update existing item
const updated = [...prev];
updated[existingIndex] = { plan: normalizedPlan, cycle };
return updated;
}
// Add new item
return [...prev, { plan: normalizedPlan, cycle }];
});
};
const removeFromCart = (planId) => {
setCart(prev => prev.filter(item => resolvePlanId(item.plan) !== planId));
};
const isPlanInCart = (planId) => {
return cart.some(item => resolvePlanId(item.plan) === planId);
};
const cartItemCount = cart.length;
const openCart = () => setIsCartOpen(true);
const closeCart = () => setIsCartOpen(false);
// Function to handle direct checkout for a single plan
const handleBuyNow$1 = (plan, cycle) => {
handleBuyNow(plan, cycle, { application: applicationUrl });
};
// Function to convert billing cycle to titlecase
const formatBillingCycle = (cycle) => {
return cycle.charAt(0).toUpperCase() + cycle.slice(1);
};
// Helper to resolve a stable plan id even if API shape differs
const resolvePlanId = (plan) => {
return (plan?.id ?? plan?.package_code_str ?? plan?.package_code ?? plan?.code ?? '');
};
// Helper function to get color scheme CSS variables
const getColorSchemeStyles = () => {
const styles = {};
// Set CSS custom properties for colors
if (colorScheme.primary) {
styles['--primary-color'] = colorScheme.primary;
styles['--primary-color-10'] = `${colorScheme.primary}1a`; // 10% opacity
styles['--primary-color-5'] = `${colorScheme.primary}0d`; // 5% opacity
styles['--primary-color-90'] = `${colorScheme.primary}e6`; // 90% opacity
}
if (colorScheme.secondary)
styles['--secondary-color'] = colorScheme.secondary;
if (colorScheme.accent)
styles['--accent-color'] = colorScheme.accent;
if (colorScheme.background)
styles['--background-color'] = colorScheme.background;
if (colorScheme.text)
styles['--text-color'] = colorScheme.text;
if (colorScheme.border)
styles['--border-color'] = colorScheme.border;
if (colorScheme.gradient?.from)
styles['--gradient-from'] = colorScheme.gradient.from;
if (colorScheme.gradient?.to)
styles['--gradient-to'] = colorScheme.gradient.to;
return styles;
};
// Helper function to get inline styles for specific elements
const getInlineStyles = (type) => {
switch (type) {
case 'primary-bg':
return colorScheme.primary ? { backgroundColor: colorScheme.primary } : {};
case 'primary-text':
return colorScheme.primary ? { color: colorScheme.primary } : {};
case 'primary-border':
return colorScheme.primary ? { borderColor: colorScheme.primary } : {};
case 'gradient-bg':
return colorScheme.gradient?.from && colorScheme.gradient?.to
? { background: `linear-gradient(to right, ${colorScheme.gradient.from}, ${colorScheme.gradient.to})` }
: {};
case 'primary-bg-10':
return colorScheme.primary ? { backgroundColor: `${colorScheme.primary}1a` } : {};
case 'primary-bg-5':
return colorScheme.primary ? { backgroundColor: `${colorScheme.primary}0d` } : {};
default:
return {};
}
};
// Default Header Component (Stocklution Style)
const DefaultHeader = ({ planType, setPlanType, sortBy, setSortBy }) => {
const hasModelPortfolios = apiData.modelPortfolios && apiData.modelPortfolios.length > 0;
return (jsxs("div", { className: "flex flex-col md:flex-row md:items-center md:justify-between mb-8 gap-4", children: [jsx("div", { className: "flex-1", children: jsx("div", { className: "w-full max-w-md", children: jsxs("div", { className: `grid p-1 rounded-full ${hasModelPortfolios ? 'grid-cols-2' : 'grid-cols-1'}`, style: getInlineStyles('primary-bg-10'), children: [jsx("button", { className: `px-4 py-2 rounded-full text-sm font-medium transition-all ${planType === 'subscriptionPlans'
? 'shadow-sm'
: ''}`, style: {
...(planType === 'subscriptionPlans'
? { backgroundColor: 'white', color: colorScheme.primary || '#3B82F6' }
: { color: colorScheme.primary || '#3B82F6' })
}, onClick: () => setPlanType('subscriptionPlans'), children: "Subscription Plans" }), hasModelPortfolios && (jsx("button", { className: `px-4 py-2 rounded-full text-sm font-medium transition-all ${planType === 'modelPortfolios'
? 'shadow-sm'
: ''}`, style: {
...(planType === 'modelPortfolios'
? { backgroundColor: 'white', color: colorScheme.primary || '#3B82F6' }
: { color: colorScheme.primary || '#3B82F6' })
}, onClick: () => setPlanType('modelPortfolios'), children: "Model Portfolios" }))] }) }) }), jsxs("div", { className: "flex items-center gap-4", children: [jsx("div", { children: jsxs("select", { className: "border rounded px-3 py-2 text-sm", style: {
backgroundColor: 'white',
borderColor: colorScheme.primary || '#3B82F6',
color: colorScheme.primary || '#3B82F6'
}, value: sortBy, onChange: (e) => setSortBy(e.target.value), children: [jsx("option", { value: "default", children: "Sort by" }), jsx("option", { value: "price-asc", children: "Price: Low to High" }), jsx("option", { value: "price-desc", children: "Price: High to Low" }), jsx("option", { value: "name", children: "Name" })] }) }), cartItemCount > 0 && (jsxs("button", { onClick: openCart, className: "relative text-white rounded-full px-4 py-2 shadow-lg hover:shadow-xl transition-all duration-200 transform hover:scale-105 flex items-center gap-2", style: getInlineStyles('gradient-bg'), children: [jsx(ShoppingCart, { className: "w-4 h-4" }), jsx("span", { className: "text-sm font-semibold", children: "Cart" }), jsx("span", { className: "bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center font-bold border border-white", children: cartItemCount })] }))] })] }));
};
// Default Plan Card Component (Stocklution Style)
const DefaultPlanCard = ({ plan, availableCycles, selectedCycle, isSelected, discountInfo, onCycleSelect, onAddToCart, isInCart, onBuyNow }) => (jsxs("div", { className: "border shadow-lg rounded-2xl h-full flex flex-col transition-all hover:shadow-2xl", children: [jsxs("div", { className: "p-6 border-b rounded-t-2xl", style: getInlineStyles('primary-bg-5'), children: [jsxs("div", { className: "flex items-center justify-between mb-1", children: [jsx("h3", { className: "text-2xl font-bold leading-tight", style: getInlineStyles('primary-text'), children: plan.package_name || plan.title }), jsx("div", { className: "text-right", children: (() => {
const discountInfo = getDiscountInfo(plan, selectedCycle);
return (jsx("div", { className: "text-lg font-bold", style: getInlineStyles('primary-text'), children: discountInfo.isDiscounted ? (jsxs("div", { className: "flex flex-col items-end", children: [jsxs("span", { className: "text-green-600", children: ["\u20B9", discountInfo.discountedPrice] }), jsxs("del", { className: "text-sm text-muted-foreground font-normal", children: ["\u20B9", discountInfo.originalPrice] })] })) : (jsxs("span", { children: ["\u20B9", discountInfo.originalPrice] })) }));
})() })] }), jsx("p", { className: "text-base text-muted-foreground mb-4", children: plan.description }), jsx("div", { className: "flex flex-col gap-2 mt-2", children: availableCycles.map((cycle) => {
const cycleDiscountInfo = getDiscountInfo(plan, cycle);
const original = plan.prices[cycle];
const discounted = plan.discounted[cycle];
const isDiscounted = discounted > 0 && discounted < original;
const savings = isDiscounted ? original - discounted : 0;
const cycleIsSelected = cycle === selectedCycle;
return (jsxs("div", { className: `flex items-center justify-between rounded-md px-3 py-2 border mb-1 text-sm cursor-pointer transition-all
${cycleIsSelected ? 'ring-2' : ''}`, style: {
...(cycleIsSelected
? { ...getInlineStyles('primary-bg-10'), ...getInlineStyles('primary-border') }
: {})
}, onClick: () => onCycleSelect(cycle), tabIndex: 0, role: "button", "aria-pressed": cycleIsSelected, onKeyDown: e => {
if (e.key === 'Enter' || e.key === ' ')
onCycleSelect(cycle);
}, children: [jsx("span", { className: "w-10 font-medium text-foreground", children: formatBillingCycle(cycle) }), jsx("span", { className: "flex items-center gap-2", children: isDiscounted ? (jsxs(Fragment, { children: [jsxs("del", { className: "line-through text-muted-foreground", children: ["\u20B9", original] }), jsxs("span", { className: "text-green-600 font-bold", children: ["\u20B9", discounted] })] })) : (jsxs("span", { className: "font-bold", children: ["\u20B9", original] })) }), savings > 0 && (jsx("span", { className: "text-sm text-green-600 text-right", children: config.showSavingsAsPercentage
? `Save ${cycleDiscountInfo.savingsPercentage}%`
: `Save ₹${savings}` }))] }, cycle));
}) })] }), jsxs("div", { className: "p-6 flex-1 flex flex-col rounded-b-2xl bg-background", children: [jsx("h4", { className: "font-medium mb-4 text-lg", children: "What's included:" }), jsx("ul", { className: "space-y-3 mb-6", children: (() => {
// Parse features using the provided logic
const parseFeatures = (featuresArr) => {
return featuresArr
.map((f) => Object.values(f)[0])
.filter((v) => typeof v === 'string' && v.trim() !== '');
};
const validFeatures = parseFeatures(plan.features);
if (validFeatures.length === 0) {
return jsx("li", { className: "text-muted-foreground text-sm", children: "No features listed." });
}
return validFeatures.map((feature, idx) => (jsxs("li", { className: "flex items-start gap-3", children: [jsx("div", { className: "h-5 w-5 rounded-full flex items-center justify-center mt-0.5 flex-shrink-0", style: getInlineStyles('primary-bg-10'), children: jsx(Check, { className: "h-3.5 w-3.5", style: getInlineStyles('primary-text') }) }), jsx("span", { className: "text-sm", children: feature })] }, idx)));
})() }), config.showCart !== false ? (jsx("button", { className: `w-full text-base h-10 font-semibold rounded-lg shadow-md transition-all flex items-center justify-center text-white transform hover:scale-[1.02] ${isInCart
? 'border-2 border-red-800 shadow-xl font-bold hover:bg-red-700'
: ''}`, style: isInCart ? { backgroundColor: '#dc2626' } : getInlineStyles('gradient-bg'), onClick: isInCart ? () => removeFromCart(resolvePlanId(plan)) : onAddToCart, children: isInCart ? 'Remove from Cart' : 'Add to Cart' })) : (jsx("button", { className: "w-full text-base h-10 flex items-center justify-center font-semibold py-3 rounded-lg shadow-md transition-all text-white transform hover:scale-[1.02]", style: getInlineStyles('gradient-bg'), onClick: () => onBuyNow(plan, selectedCycle), children: "Buy Now" }))] })] }));
if (loading) {
return (jsx("div", { className: `w-full ${className}`, children: jsx("div", { className: "flex items-center justify-center py-20", children: jsxs("div", { className: "text-center", children: [jsx(LoaderCircle, { className: "h-12 w-12 animate-spin mx-auto mb-4", style: { color: colorScheme.primary || '#3B82F6' } }), jsx("p", { className: "text-muted-foreground", children: "Loading subscription plans..." })] }) }) }));
}
if (error) {
return (jsx("div", { className: `w-full ${className}`, children: jsx("div", { className: "flex items-center justify-center py-20", children: jsxs("div", { className: "text-center", children: [jsxs("p", { className: "text-destructive mb-4", children: ["Error: ", error] }), jsx("button", { onClick: () => window.location.reload(), className: "px-4 py-2 text-white rounded-lg transition-colors", style: getInlineStyles('primary-bg'), children: "Try Again" })] }) }) }));
}
return (jsxs("div", { className: `w-full ${className}`, style: getColorSchemeStyles(), children: [renderHeader ? (renderHeader({ planType, setPlanType, sortBy, setSortBy })) : (jsx(DefaultHeader, { planType: planType, setPlanType: setPlanType, sortBy: sortBy, setSortBy: setSortBy })), jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-8", children: displayedPlans.map((plan, index) => {
const availableCycles = getAvailableCycles(plan);
// Use a robust plan id
const planId = resolvePlanId(plan) || `plan-${index}`;
// Helper function to find minimum price cycle
const getMinimumPriceCycle = (prices) => {
const validPrices = Object.entries(prices)
.filter(([_, price]) => price > 0)
.sort(([_, a], [__, b]) => a - b);
return validPrices.length > 0 ? validPrices[0][0] : 'monthly';
};
// Use selected cycle or fallback to minimum price cycle
const selectedCycle = selectedCycles[planId] || getMinimumPriceCycle(plan.prices);
const discountInfo = getDiscountInfo(plan, selectedCycle);
const isInCart = isPlanInCart(planId);
const planCardProps = {
plan,
availableCycles,
selectedCycle,
isSelected: selectedCycles[planId] === selectedCycle || (!selectedCycles[planId] && selectedCycle === getMinimumPriceCycle(plan.prices)),
discountInfo,
onCycleSelect: (cycle) => {
setSelectedCycle(planId, cycle);
// If plan is in cart, update it with new cycle
if (isPlanInCart(planId)) {
removeFromCart(planId);
addToCart(plan, cycle);
}
},
onAddToCart: () => addToCart(plan, selectedCycle),
isInCart,
onBuyNow: handleBuyNow$1
};
return (jsx("div", { children: renderPlanCard ? (jsx("div", { children: renderPlanCard(planCardProps) }, `custom-plan-${planId}`)) : (jsx(DefaultPlanCard, { ...planCardProps }, `default-plan-${planId}`)) }, planId));
}) }), renderCart && (jsx("div", { children: React.isValidElement(renderCart({ cart, isCartOpen, closeCart, removeFromCart }))
? renderCart({ cart, isCartOpen, closeCart, removeFromCart })
: jsx("div", { children: "Invalid cart component" }) }, "custom-cart")), !renderCart && (jsx(Cart, { cart: cart, isOpen: isCartOpen, onClose: closeCart, onRemoveItem: removeFromCart, applicationUrl: applicationUrl, colorScheme: colorScheme }, "default-cart"))] }));
};
export { Cart, Subscription, calculateCartTotal, calculateDiscountInfo, createCheckoutPayload, filterPlansBySearch, formatPrice, generateCheckoutUrl, getAvailableCycles, getBestDiscountPercentage, getMinimumPriceCycle, handleBuyNow, hasDiscounts, parseFeatures, parsePlan, sortPlans, useSubscription, validatePlan };
//# sourceMappingURL=index.esm.js.map