diff --git a/src/components/Pricing/PricingAddons/index.js b/src/components/Pricing/PricingAddons/index.js index a54f031bb0f2c..0e6176d262306 100644 --- a/src/components/Pricing/PricingAddons/index.js +++ b/src/components/Pricing/PricingAddons/index.js @@ -39,16 +39,18 @@ import { getToggleButtonStyle, getSliderStyle } from "./styles"; -import { formatAndConvertPrice, formatSliderPrice } from "../../../utils/currencies"; -export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterprisePlan }) => { +import { formatAndConvertPrice } from "../../../utils/currencies"; + +export const PricingAddons = ({ isYearly = false, setIsYearly ,currency,enterprisePlan }) => { const [selectedAddon, setSelectedAddon] = useState(null); - // const [quantity, setQuantity] = useState(1); + const [addonMenuOpen, setAddonMenuOpen] = useState(false); const quantity = 1; const [selectedSubAddOns, setSelectedSubAddOns] = useState({}); const [totalPrice, setTotalPrice] = useState(0); const [quantityIndex, setQuantityIndex] = useState(0); const [enterpriseUsers, setEnterpriseUsers] = useState(1); + const [perAddonState, setPerAddonState] = useState({}); const { isDark } = useStyledDarkMode(); const theme = useTheme(); @@ -57,14 +59,49 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr return getAddOns(); }, []); + useEffect(() => { + if (!addonMenuOpen) return; + const y = window.scrollY || window.pageYOffset || 0; + + + const html = document.documentElement; + const body = document.body; + + const prevHtmlOverflow = html.style.overflow; + const prevHtmlTouchAction = html.style.touchAction; + const prevBodyOverflow = body.style.overflow; + const prevBodyPosition = body.style.position; + const prevBodyTop = body.style.top; + const prevBodyWidth = body.style.width; + + html.style.overflow = "hidden"; + html.style.touchAction = "none"; + body.style.overflow = "hidden"; + body.style.position = "fixed"; + body.style.top = `-${y}px`; + body.style.width = "100%"; + + return () => { + html.style.overflow = prevHtmlOverflow; + html.style.touchAction = prevHtmlTouchAction; + body.style.overflow = prevBodyOverflow; + body.style.position = prevBodyPosition; + body.style.top = prevBodyTop; + body.style.width = prevBodyWidth; + window.scrollTo(0, y); + }; + }, [addonMenuOpen]); + // Helper function to render icons based on type const renderIcon = (iconType) => { switch (iconType) { - case "academy": - return ; + case "academy": + return ( + + ); case "cloud": return ; case "group": @@ -74,15 +111,13 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr } }; - const formatPrice = (price) => { - return formatAndConvertPrice(price, currency); - }; + const formatPrice = (price) => formatAndConvertPrice(price, currency); useEffect(() => { if (selectedAddon) { let baseTotal = 0; if (selectedAddon.id === "academy") { - const theorySubAddon = selectedAddon.subAddOns?.find(sub => sub.id === "academy-theory"); + const theorySubAddon = selectedAddon.subAddOns?.find((sub) => sub.id === "academy-theory"); if (theorySubAddon?.pricing && theorySubAddon.pricing[quantityIndex]) { const currentLearnerOption = theorySubAddon.pricing[quantityIndex]; const monthlyPerUserCost = currentLearnerOption.monthlyPerUser; @@ -91,20 +126,18 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr ? yearlyPerUserCost * currentLearnerOption.learners : monthlyPerUserCost * currentLearnerOption.learners; } - } else { - if (selectedAddon.pricing && selectedAddon.pricing[quantityIndex]) { - const currentOption = selectedAddon.pricing[quantityIndex]; - const monthlyPerUnitCost = currentOption.monthlyPerUnit; - const yearlyPerUnitCost = currentOption.yearlyPerUnit; - baseTotal = isYearly - ? yearlyPerUnitCost * currentOption.units - : monthlyPerUnitCost * currentOption.units; - } + } else if (selectedAddon.pricing && selectedAddon.pricing[quantityIndex]) { + const currentOption = selectedAddon.pricing[quantityIndex]; + const monthlyPerUnitCost = currentOption.monthlyPerUnit; + const yearlyPerUnitCost = currentOption.yearlyPerUnit; + baseTotal = isYearly + ? yearlyPerUnitCost * currentOption.units + : monthlyPerUnitCost * currentOption.units; } let subAddOnTotal = 0; if (selectedAddon?.id === "academy" && selectedAddon.subAddOns) { - selectedAddon.subAddOns.forEach(subAddOn => { + selectedAddon.subAddOns.forEach((subAddOn) => { if (selectedSubAddOns[subAddOn.id] && subAddOn.id !== "academy-theory") { const subAddOnPricing = subAddOn.pricing && subAddOn.pricing[quantityIndex]; if (subAddOnPricing) { @@ -115,34 +148,59 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr }); } + const enterpriseUsersCost = (isYearly ? enterprisePlan.yearlyprice : enterprisePlan.monthlyprice) * (enterpriseUsers > 0 ? enterpriseUsers : 1); setTotalPrice(baseTotal + subAddOnTotal + enterpriseUsersCost); } else { setTotalPrice(0); } - }, [selectedAddon, quantity, quantityIndex, selectedSubAddOns, isYearly, enterpriseUsers, enterprisePlan]); + }, [selectedAddon, quantityIndex, selectedSubAddOns, isYearly, enterpriseUsers, enterprisePlan]); + + const getDefaultAddonState = (addon) => ({ + quantityIndex: 0, + selectedSubAddOns: addon?.id === "academy" ? { "academy-theory": true } : {}, + }); const handleAddonChange = (addonId) => { const addon = addOns.find((a) => a.id === addonId); - setSelectedAddon(addon || null); - setQuantityIndex(0); - // Always select "academy-theory" if academy is chosen - if (addon?.id === "academy") { - setSelectedSubAddOns({ "academy-theory": true }); - } else { - setSelectedSubAddOns({}); + if (selectedAddon) { + setPerAddonState((prev) => ({ + ...prev, + [selectedAddon.id]: { quantityIndex, selectedSubAddOns }, + })); } + + setSelectedAddon(addon || null); + setAddonMenuOpen(false); + + if (!addon) return; + + const restored = perAddonState[addon.id] || getDefaultAddonState(addon); + setQuantityIndex(restored.quantityIndex ?? 0); + setSelectedSubAddOns(restored.selectedSubAddOns ?? {}); }; const handleSubAddOnToggle = (subAddOnId, isChecked) => { - setSelectedSubAddOns(prev => ({ - ...prev, - [subAddOnId]: isChecked - })); + setSelectedSubAddOns((prev) => { + const next = { ...prev, [subAddOnId]: isChecked }; + // keep cache updated for current addon + if (selectedAddon) { + setPerAddonState((cache) => ({ + ...cache, + [selectedAddon.id]: { + ...(cache[selectedAddon.id] || getDefaultAddonState(selectedAddon)), + quantityIndex, + selectedSubAddOns: next, + }, + })); + } + return next; + }); }; + const getPlanLinkForAcademy = () => { if (!selectedAddon || selectedAddon.id !== "academy") { return { link: "#", name: "Subscribe" }; @@ -151,6 +209,11 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr let targetSubAddon = null; if (selectedSubAddOns["academy-practical"]) { + targetSubAddon = selectedAddon.subAddOns?.find((sub) => sub.id === "academy-practical"); + targetSubAddonName = "with Practical Learning"; + } else { + targetSubAddon = selectedAddon.subAddOns?.find((sub) => sub.id === "academy-theory"); + targetSubAddonName = ""; targetSubAddon = selectedAddon.subAddOns?.find(sub => sub.id === "academy-practical"); } else { targetSubAddon = selectedAddon.subAddOns?.find(sub => sub.id === "academy-theory"); @@ -164,13 +227,15 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr const currentLearnerCount = targetSubAddon.pricing[quantityIndex].learners; const matchingPlanLink = targetSubAddon.planLink.find( - plan => plan.cadence === currentCadence && plan.learners === currentLearnerCount + (plan) => plan.cadence === currentCadence && plan.learners === currentLearnerCount ); if (matchingPlanLink) { + const enterpriseUserSeats = enterpriseUsers > 0 ? ` and ${enterpriseUsers} enterprise +user${enterpriseUsers > 1 ? "s" : ""}` : ""; return { link: matchingPlanLink.link, - name: "Subscribe" + name: `Subscribe (${currentLearnerCount} learners${targetSubAddonName ? " " + targetSubAddonName : ""}${enterpriseUserSeats})` }; } @@ -184,13 +249,36 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr return { link: "#", - name: "Subscribe" + name: `Subscribe to ${selectedAddon.name}` }; }; return ( + {addonMenuOpen && ( + setAddonMenuOpen(false)} + onWheel={(e) => { + e.preventDefault(); e.stopPropagation(); + }} + onTouchMove={(e) => { + e.preventDefault(); e.stopPropagation(); + }} + onScroll={(e) => { + e.preventDefault(); e.stopPropagation(); + }} + sx={{ + position: "fixed", + inset: 0, + zIndex: (t) => ((t?.zIndex?.menu ?? 1200) - 1), + backgroundColor: "transparent", + pointerEvents: "auto", + touchAction: "none", + }} + /> + )} + handleAddonChange(e.target.value)} label="Optionally, choose one or more add-ons" + onOpen={() => setAddonMenuOpen(true)} + onClose={() => setAddonMenuOpen(false)} MenuProps={{ - disableScrollLock: true, - disablePortal: true, + disablePortal: false, + PaperProps: { + sx: { maxHeight: "60vh" }, + onScrollCapture: (e) => e.stopPropagation(), + }, }} > {addOns.map((addon) => ( @@ -245,7 +338,7 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr {addon.name} - + {addon.id === "academy" ? addon.description : (() => { @@ -271,10 +364,14 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr handleSubAddOnToggle("academy-theory", e.target.checked)} - color="primary" />} + control={ + handleSubAddOnToggle("academy-theory", e.target.checked)} + color="primary" + /> + } sx={formControlStyles.base} /> - {selectedAddon?.subAddOns?.find(sub => sub.id === "academy-theory")?.features?.map((feature, index) => ( + {selectedAddon?.subAddOns?.find((sub) => sub.id === "academy-theory")?.features?.map((feature, index) => ( } @@ -298,10 +395,14 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr handleSubAddOnToggle("academy-practical", e.target.checked)} - color="primary" />} + control={ + handleSubAddOnToggle("academy-practical", e.target.checked)} + color="primary" + /> + } sx={formControlStyles.base} /> - {selectedAddon?.subAddOns?.find(sub => sub.id === "academy-practical")?.features?.map((feature, index) => ( + {selectedAddon?.subAddOns?.find((sub) => sub.id === "academy-practical")?.features?.map((feature, index) => ( } @@ -324,25 +425,49 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr + + {(() => { + // Determine which sub-addon to show learner count for + let targetSubAddon = null; + if (selectedSubAddOns["academy-practical"]) { + targetSubAddon = selectedAddon?.subAddOns?.find((sub) => sub.id === "academy-practical"); + } else { + targetSubAddon = selectedAddon?.subAddOns?.find((sub) => sub.id === "academy-theory"); + } + return targetSubAddon?.pricing?.[quantityIndex]?.learners || 0; + })()}{" "} Learners + setQuantityIndex(newValue)} + onChange={(event, newValue) => { + setQuantityIndex(newValue); + // update cache for current addon + if (selectedAddon) { + setPerAddonState((prev) => ({ + ...prev, + [selectedAddon.id]: { + ...(prev[selectedAddon.id] || getDefaultAddonState(selectedAddon)), + quantityIndex: newValue, + selectedSubAddOns, + }, + })); + } + }} min={0} valueLabelDisplay="auto" valueLabelFormat={(value) => { // Determine which sub-addon to show pricing for let targetSubAddon = null; if (selectedSubAddOns["academy-practical"]) { - targetSubAddon = selectedAddon?.subAddOns?.find(sub => sub.id === "academy-practical"); + targetSubAddon = selectedAddon?.subAddOns?.find((sub) => sub.id === "academy-practical"); } else { - targetSubAddon = selectedAddon?.subAddOns?.find(sub => sub.id === "academy-theory"); + targetSubAddon = selectedAddon?.subAddOns?.find((sub) => sub.id === "academy-theory"); } if (targetSubAddon?.pricing && targetSubAddon.pricing[value]) { const option = targetSubAddon.pricing[value]; const pricePerUser = isYearly ? option.yearlyPerUser : option.monthlyPerUser; - const multiplier = selectedSubAddOns["academy-practical"] ? 2 : 1; - const totalPrice = pricePerUser * option.learners * multiplier; + const totalPrice = pricePerUser * option.learners; const period = isYearly ? "/year" : "/month"; return `${option.learners} learners - ${formatPrice(totalPrice)}${period}`; } @@ -352,9 +477,9 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr // Determine which sub-addon to use for max value let targetSubAddon = null; if (selectedSubAddOns["academy-practical"]) { - targetSubAddon = selectedAddon?.subAddOns?.find(sub => sub.id === "academy-practical"); + targetSubAddon = selectedAddon?.subAddOns?.find((sub) => sub.id === "academy-practical"); } else { - targetSubAddon = selectedAddon?.subAddOns?.find(sub => sub.id === "academy-theory"); + targetSubAddon = selectedAddon?.subAddOns?.find((sub) => sub.id === "academy-theory"); } return (targetSubAddon?.pricing?.length - 1) || 0; })()} @@ -364,11 +489,20 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr // Determine which sub-addon to show pricing for based on selection let targetSubAddon = null; if (selectedSubAddOns["academy-practical"]) { - targetSubAddon = selectedAddon?.subAddOns?.find(sub => sub.id === "academy-practical"); + targetSubAddon = selectedAddon?.subAddOns?.find((sub) => sub.id === "academy-practical"); } else { - targetSubAddon = selectedAddon?.subAddOns?.find(sub => sub.id === "academy-theory"); + targetSubAddon = selectedAddon?.subAddOns?.find((sub) => sub.id === "academy-theory"); } - + return ( + targetSubAddon?.pricing?.map((option, index) => ({ + value: index, + label: ( + + {option.learners === "2500+" ? "2,500+" : option.learners} + ({ value: index, label: ( @@ -381,6 +515,9 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr xs: "0.75rem", sm: "0.9rem", } + }}> + {formatPrice(isYearly ? option.yearlyPerUser : option.monthlyPerUser)}
{targetSubAddon.unitLabelSingular}/{isYearly ? "year" : "month"} +
}} > {formatSliderPrice((option.yearlyPerUser / 12) * (selectedSubAddOns["academy-practical"] ? 2 : 1), currency)}
{targetSubAddon.unitLabelSingular}/month @@ -397,13 +534,13 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr }}> {formatSliderPrice((isYearly ? option.yearlyPerUser : option.monthlyPerUser) * (selectedSubAddOns["academy-practical"] ? 2 : 1), currency)}
{targetSubAddon.unitLabelSingular}/{isYearly ? "year" : "month"}
-
- ), - })) || []; + ), + })) || [] + ); })()} /> - + Looking for a plan larger than 2,500 learners? Great! Let us know. @@ -414,9 +551,25 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr {selectedAddon !== null && selectedAddon.id !== "academy" && ( <> + + {selectedAddon.pricing?.[quantityIndex]?.units || 0} {selectedAddon?.unitLabel} + setQuantityIndex(newValue)} + onChange={(event, newValue) => { + setQuantityIndex(newValue); + // update cache for current addon + if (selectedAddon) { + setPerAddonState((prev) => ({ + ...prev, + [selectedAddon.id]: { + ...(prev[selectedAddon.id] || getDefaultAddonState(selectedAddon)), + quantityIndex: newValue, + selectedSubAddOns, + }, + })); + } + }} min={0} max={selectedAddon?.pricing?.length - 1 || 0} step={null} @@ -438,6 +591,7 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr {option.units} + {formatPrice(isYearly ? option.yearlyPerUnit * option.units : option.monthlyPerUnit * option.units)} {formatSliderPrice(isYearly ? option.yearlyPerUnit * option.units : option.monthlyPerUnit * option.units, currency)} @@ -446,20 +600,19 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr /> - )} + )}
{selectedAddon && ( - - Add-on × Quantity / per Subscription Duration + Add-on × Quantity / per Subscription Duration SUBTOTAL @@ -468,39 +621,30 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr - - {selectedAddon?.id === "academy" - ? `Theoretical Learning × ${selectedAddon?.subAddOns?.find(sub => sub.id === "academy-theory")?.pricing?.[quantityIndex]?.learners || 0}` - : `${selectedAddon?.name} × ${selectedAddon?.pricing?.[quantityIndex]?.units || 0}` - } - - - {(() => { - if (selectedAddon?.id === "academy") { - const theorySubAddon = selectedAddon?.subAddOns?.find(sub => sub.id === "academy-theory"); - if (theorySubAddon?.pricing && theorySubAddon.pricing[quantityIndex]) { - const currentLearnerOption = theorySubAddon.pricing[quantityIndex]; - const monthlyPerUserCost = currentLearnerOption.monthlyPerUser; - const yearlyPerUserCost = currentLearnerOption.yearlyPerUser; - const totalCost = isYearly - ? yearlyPerUserCost * currentLearnerOption.learners - : monthlyPerUserCost * currentLearnerOption.learners; - return formatPrice(totalCost); - } - return formatPrice(0); - } else { - if (selectedAddon?.pricing && selectedAddon.pricing[quantityIndex]) { - const currentOption = selectedAddon.pricing[quantityIndex]; - const monthlyPerUnitCost = currentOption.monthlyPerUnit; - const yearlyPerUnitCost = currentOption.yearlyPerUnit; - const totalCost = isYearly - ? yearlyPerUnitCost * currentOption.units - : monthlyPerUnitCost * currentOption.units; - return formatPrice(totalCost); - } - return formatPrice(0); + + {selectedAddon?.id === "academy" ? + `Theoretical Learning × ${selectedAddon?.subAddOns?.find(sub => sub.id === "academy-theory")?.pricing?.[quantityIndex]?.learners || 0}` : + `${selectedAddon?.name} × ${quantity} x ${selectedAddon?.cadence}` + } + + + {(() => { + if (selectedAddon?.id === "academy") { + const theorySubAddon = selectedAddon?.subAddOns?.find(sub => sub.id === "academy-theory"); + if (theorySubAddon?.pricing && theorySubAddon.pricing[quantityIndex]) { + const currentLearnerOption = theorySubAddon.pricing[quantityIndex]; + const monthlyPerUserCost = currentLearnerOption.monthlyPerUser; + const yearlyPerUserCost = currentLearnerOption.yearlyPerUser; + const totalCost = isYearly + ? yearlyPerUserCost * currentLearnerOption.learners + : monthlyPerUserCost * currentLearnerOption.learners; + return formatPrice(totalCost); + } + return formatPrice(0); + } else { + return formatPrice((isYearly ? selectedAddon?.yearlyPrice : selectedAddon?.monthlyPrice) * quantity); } - })()}/{isYearly ? "yearly" : "monthly"} + })()} @@ -508,7 +652,7 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr selectedSubAddOns[subAddOn.id] && subAddOn.id !== "academy-theory" && ( - {subAddOn.name} × {subAddOn.pricing?.[quantityIndex]?.learners || 0} + {subAddOn.name} × {subAddOn.pricing?.[quantityIndex]?.learners || 0}/{isYearly ? "yearly" : "monthly"} {( @@ -548,7 +692,9 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr {formatPrice((isYearly ? enterprisePlan.yearlyprice : enterprisePlan.monthlyprice) * (enterpriseUsers > 0 ? enterpriseUsers : 1))}/{isYearly ? "yearly" : "monthly"} - + + TOTAL + Total Cost @@ -580,4 +726,4 @@ export const PricingAddons = ({ isYearly = false, setIsYearly, currency, enterpr
); -}; \ No newline at end of file +};