diy-template-components 5.12.0 → 5.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.es.js CHANGED
@@ -349,7 +349,7 @@ const useSectionStyles$b = createUseStyles(theme => ({
349
349
  bottom: 0,
350
350
  left: 0,
351
351
  width: '100%',
352
- zIndex: '100'
352
+ zIndex: '10000'
353
353
  }
354
354
  },
355
355
  mobileAppNameClass: {
@@ -609,7 +609,11 @@ const Button = /*#__PURE__*/forwardRef(function Button({
609
609
  rel: data?.isExternal ? 'nofollow noopener' : null,
610
610
  className: (active ? classes.active : '') + ' ' + classes[type] + ' ' + classes[size] + ' ' + classes.anchorClass,
611
611
  style: styling
612
- }, elementProps), data?.value) : /*#__PURE__*/React.createElement("button", _extends({
612
+ }, elementProps), /*#__PURE__*/React.createElement("span", {
613
+ dangerouslySetInnerHTML: {
614
+ __html: data?.value || ''
615
+ }
616
+ })) : /*#__PURE__*/React.createElement("button", _extends({
613
617
  ref: ref,
614
618
  className: (active ? classes.active : '') + ' ' + classes[type] + ' ' + classes[size],
615
619
  onClick: isEdit ? null : onClick,
@@ -10910,7 +10914,7 @@ const SingleVideoSlide$1 = props => {
10910
10914
  currencySymbol
10911
10915
  } = props;
10912
10916
  const showCourseInstallmentData = data?.courseOverviewData;
10913
- const InstalmentData = isEdit ? data?.courseOverviewData?.installments?.formData?.installments[0].installmentAmount : data?.courseOverviewData?.installments?.formData?.installmentInfo?.installments[0].installmentPrice;
10917
+ const InstalmentData = isEdit ? data?.courseOverviewData?.installments?.formData?.installments[0].installmentAmount : data?.courseOverviewData?.installments?.formData?.installmentInfo?.installments?.[0]?.installmentPrice;
10914
10918
  const checkIfOfferIsValid = () => moment().diff(moment(props?.data?.endDate || 0)) < 0;
10915
10919
  const checkForShowDiscount = () => {
10916
10920
  if (props.data.endDate === null || checkIfOfferIsValid()) {
@@ -12609,29 +12613,125 @@ const useCounterSectionStyles = createUseStyles(theme => {
12609
12613
  const DURATION_MS = 2000;
12610
12614
  const EASING = t => 1 - Math.pow(1 - t, 3); // easeOutCubic
12611
12615
 
12612
- function parseCounterValue(value) {
12616
+ function getPlainTextFromHtml(html) {
12617
+ if (typeof document === 'undefined') {
12618
+ return String(html).replace(/<[^>]*>/g, '');
12619
+ }
12620
+ const div = document.createElement('div');
12621
+ div.innerHTML = html;
12622
+ return div.textContent || div.innerText || '';
12623
+ }
12624
+ function parseStyleString(styleStr) {
12625
+ if (!styleStr || typeof styleStr !== 'string') return {};
12626
+ const obj = {};
12627
+ styleStr.split(';').forEach(part => {
12628
+ const colonIdx = part.indexOf(':');
12629
+ if (colonIdx > 0) {
12630
+ const key = part.slice(0, colonIdx).trim();
12631
+ const val = part.slice(colonIdx + 1).trim();
12632
+ if (key && val) obj[key] = val;
12633
+ }
12634
+ });
12635
+ return obj;
12636
+ }
12637
+ const SEMANTIC_TAG_STYLES = {
12638
+ strong: {
12639
+ fontWeight: 'bold'
12640
+ },
12641
+ b: {
12642
+ fontWeight: 'bold'
12643
+ },
12644
+ em: {
12645
+ fontStyle: 'italic'
12646
+ },
12647
+ i: {
12648
+ fontStyle: 'italic'
12649
+ },
12650
+ u: {
12651
+ textDecoration: 'underline'
12652
+ }
12653
+ };
12654
+
12655
+ /**
12656
+ * Parses value (plain text or HTML) to extract number for animation and wrapper attributes.
12657
+ * Returns { end, suffix, wrapperProps } or null if not parseable.
12658
+ * wrapperProps: { className, style } for the rich text wrapper, or null if plain text.
12659
+ */
12660
+ function parseCounterValueFromHtml(value) {
12613
12661
  if (value == null || value === '') return null;
12614
- const match = String(value).trim().match(/^([\d.]+)(.*)$/);
12662
+ const str = String(value).trim();
12663
+ const plainText = /<[^>]+>/.test(str) ? getPlainTextFromHtml(str) : str;
12664
+ const match = plainText.match(/^([\d.]+)(.*)$/);
12615
12665
  if (!match) return null;
12616
12666
  const num = parseFloat(match[1]);
12617
12667
  if (Number.isNaN(num)) return null;
12618
- return {
12619
- end: num,
12620
- suffix: match[2] || ''
12621
- };
12668
+ const suffix = match[2] || '';
12669
+ if (!/<[^>]+>/.test(str)) {
12670
+ return {
12671
+ end: num,
12672
+ suffix,
12673
+ wrapperProps: null
12674
+ };
12675
+ }
12676
+ if (typeof document === 'undefined') {
12677
+ return {
12678
+ end: num,
12679
+ suffix,
12680
+ wrapperProps: null
12681
+ };
12682
+ }
12683
+ try {
12684
+ const parser = new DOMParser();
12685
+ const doc = parser.parseFromString(str, 'text/html');
12686
+ const root = doc.body?.firstElementChild;
12687
+ if (!root) return {
12688
+ end: num,
12689
+ suffix,
12690
+ wrapperProps: null
12691
+ };
12692
+ const classes = [];
12693
+ const styleObj = {};
12694
+ const collectFromElement = el => {
12695
+ const c = el.getAttribute('class');
12696
+ if (c) classes.push(...c.split(/\s+/).filter(Boolean));
12697
+ const s = el.getAttribute('style');
12698
+ if (s) Object.assign(styleObj, parseStyleString(s));
12699
+ const tagStyles = SEMANTIC_TAG_STYLES[el.tagName?.toLowerCase()];
12700
+ if (tagStyles) Object.assign(styleObj, tagStyles);
12701
+ for (const child of el.children) {
12702
+ collectFromElement(child);
12703
+ }
12704
+ };
12705
+ collectFromElement(root);
12706
+ const className = [...new Set(classes)].join(' ');
12707
+ return {
12708
+ end: num,
12709
+ suffix,
12710
+ wrapperProps: className || Object.keys(styleObj).length ? {
12711
+ className,
12712
+ style: styleObj
12713
+ } : null
12714
+ };
12715
+ } catch {
12716
+ return {
12717
+ end: num,
12718
+ suffix,
12719
+ wrapperProps: null
12720
+ };
12721
+ }
12622
12722
  }
12623
12723
  function AnimatedCounter({
12624
12724
  value,
12625
12725
  className,
12626
12726
  refSetter
12627
12727
  }) {
12628
- const elRef = useRef(null);
12728
+ const animSpanRef = useRef(null);
12629
12729
  const rafIdRef = useRef(null);
12630
12730
  const hasAnimatedRef = useRef(false);
12631
12731
  useLayoutEffect(() => {
12632
12732
  hasAnimatedRef.current = false; // Reset so we can re-animate when value changes (e.g. live edit)
12633
- const parsed = parseCounterValue(value);
12634
- if (!parsed || !elRef.current) return;
12733
+ const parsed = parseCounterValueFromHtml(value);
12734
+ if (!parsed || !animSpanRef.current) return;
12635
12735
  const observer = new IntersectionObserver(entries => {
12636
12736
  const [entry] = entries;
12637
12737
  if (!entry.isIntersecting || hasAnimatedRef.current) return;
@@ -12646,9 +12746,9 @@ function AnimatedCounter({
12646
12746
  const progress = Math.min(elapsed / DURATION_MS, 1);
12647
12747
  const eased = EASING(progress);
12648
12748
  const current = eased * end;
12649
- if (elRef.current) {
12749
+ if (animSpanRef.current) {
12650
12750
  const displayValue = Number.isInteger(end) ? Math.round(current) : parseFloat(current.toFixed(2));
12651
- elRef.current.textContent = displayValue + suffix;
12751
+ animSpanRef.current.textContent = displayValue + suffix;
12652
12752
  }
12653
12753
  if (progress < 1) {
12654
12754
  rafIdRef.current = requestAnimationFrame(tick);
@@ -12658,22 +12758,34 @@ function AnimatedCounter({
12658
12758
  }, {
12659
12759
  threshold: 0.2
12660
12760
  });
12661
- observer.observe(elRef.current);
12761
+ observer.observe(animSpanRef.current);
12662
12762
  return () => {
12663
12763
  observer.disconnect();
12664
12764
  if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current);
12665
12765
  };
12666
12766
  }, [value]);
12667
- const parsed = parseCounterValue(value);
12767
+ const parsed = parseCounterValueFromHtml(value);
12668
12768
  const setRef = el => {
12669
- elRef.current = el;
12670
12769
  refSetter?.(el);
12671
12770
  };
12672
12771
  if (parsed) {
12772
+ const {
12773
+ end,
12774
+ suffix,
12775
+ wrapperProps
12776
+ } = parsed;
12777
+ const initialDisplay = Number.isInteger(end) ? '0' : '0.00';
12778
+ const animatedContent = /*#__PURE__*/React.createElement("span", {
12779
+ ref: animSpanRef
12780
+ }, initialDisplay, suffix);
12781
+ const content = wrapperProps ? /*#__PURE__*/React.createElement("span", {
12782
+ className: wrapperProps.className || undefined,
12783
+ style: Object.keys(wrapperProps.style || {}).length ? wrapperProps.style : undefined
12784
+ }, animatedContent) : animatedContent;
12673
12785
  return /*#__PURE__*/React.createElement("h2", {
12674
12786
  ref: setRef,
12675
12787
  className: className
12676
- }, "0", parsed.suffix);
12788
+ }, content);
12677
12789
  }
12678
12790
  return /*#__PURE__*/React.createElement("h2", {
12679
12791
  ref: refSetter,
@@ -12725,6 +12837,7 @@ function CounterSection({
12725
12837
  key: item._id || index,
12726
12838
  className: classes.counterItem
12727
12839
  }, value != null && value !== '' && /*#__PURE__*/React.createElement(AnimatedCounter, {
12840
+ key: `${item._id || index}-${/<[^>]+>/.test(value || '') ? 'html' : 'plain'}`,
12728
12841
  value: value,
12729
12842
  className: classes.value,
12730
12843
  refSetter: getValueRefSetter(item)
@@ -12852,8 +12965,8 @@ const getFloatingButtonBase = theme => ({
12852
12965
  zIndex: 9999,
12853
12966
  display: 'inline-flex',
12854
12967
  alignItems: 'center',
12855
- gap: '10px',
12856
- padding: '12px 20px',
12968
+ gap: '8px',
12969
+ padding: '14px',
12857
12970
  backgroundColor: theme?.colors?.ctaColor || '#FFFFFF',
12858
12971
  border: `1px solid ${theme?.palette?.border?.secondary ?? '#E5E7EB'}`,
12859
12972
  borderRadius: '9999px',
@@ -12863,7 +12976,6 @@ const getFloatingButtonBase = theme => ({
12863
12976
  fontFamily: theme?.typography?.fontFamily ?? 'inherit',
12864
12977
  fontWeight: theme?.typography?.fontWeight?.bold ?? 700,
12865
12978
  fontSize: '14px',
12866
- textTransform: 'uppercase',
12867
12979
  letterSpacing: '0.02em',
12868
12980
  color: theme?.colors?.CtaTextColor || 'white',
12869
12981
  transition: 'box-shadow 0.2s ease, transform 0.2s ease'
@@ -12875,7 +12987,7 @@ const getFloatingButtonResponsive = theme => {
12875
12987
  ...base,
12876
12988
  bottom: 142,
12877
12989
  left: 16,
12878
- padding: '10px 16px',
12990
+ padding: '10px',
12879
12991
  gap: '8px',
12880
12992
  fontSize: '12px',
12881
12993
  background: theme?.colors?.ctaColor || '#FFFFFF'
@@ -12908,6 +13020,17 @@ const whatsAppIconSvgSize = {
12908
13020
  };
12909
13021
  const getWhatsAppIconSize = breakpoint => whatsAppIconSvgSize[breakpoint] ?? whatsAppIconSvgSize.mobile;
12910
13022
 
13023
+ /**
13024
+ * Height of the sticky bar (full-width CTA) by breakpoint for page padding.
13025
+ * Used to prevent content overlap when the sticky bar is fixed at the bottom.
13026
+ * Values match the sticky bar's padding + content height per breakpoint.
13027
+ */
13028
+ const STICKY_BAR_HEIGHT_BY_BREAKPOINT = {
13029
+ mobile: 0,
13030
+ tablet: 78,
13031
+ desktop: 82
13032
+ };
13033
+
12911
13034
  const DEFAULT_WHATSAPP_ICON_COLOR = '#25D366';
12912
13035
 
12913
13036
  /** Build WhatsApp wa.me URL from phone number (digits only, optional + prefix). */
@@ -12999,6 +13122,7 @@ function StickyCta({
12999
13122
  const contentList = node?.contentList?.components || [];
13000
13123
  const showEditHint = sectionData?.isDefaultEditor ?? false;
13001
13124
  sectionData?.bgSection?.components?.[0]?.sectionBgData?.metadata?.value;
13125
+ const redirectUrl = node?.subheading?.metadata?.value || '';
13002
13126
  const propsFromSection = node ? {
13003
13127
  type: node?.title?.metadata?.value ?? type,
13004
13128
  stickyMessage: contentList[1]?.contentPara?.metadata?.value ?? stickyMessage,
@@ -13021,7 +13145,7 @@ function StickyCta({
13021
13145
  whatsAppButtonBackgroundColor,
13022
13146
  iconImageUrl
13023
13147
  };
13024
- const whatsAppUrl = buildWhatsAppUrl(p?.whatsAppPhoneNumber) ?? p?.whatsAppHref ?? '#';
13148
+ buildWhatsAppUrl(p?.whatsAppPhoneNumber) ?? p?.whatsAppHref ?? '#';
13025
13149
  const theme = useTheme() || {};
13026
13150
  const [breakpoint, setBreakpoint] = useState(() => typeof window !== 'undefined' ? getBreakpoint(window.innerWidth, isMobile) : 'mobile');
13027
13151
  useEffect(() => {
@@ -13060,7 +13184,7 @@ function StickyCta({
13060
13184
  const iconSize = getWhatsAppIconSize(breakpoint);
13061
13185
  const iconColor = p.whatsAppIconColor ?? DEFAULT_WHATSAPP_ICON_COLOR;
13062
13186
  return /*#__PURE__*/React.createElement(React.Fragment, null, isMobile ? null : editHintBlock, /*#__PURE__*/React.createElement("a", {
13063
- href: whatsAppUrl,
13187
+ href: redirectUrl,
13064
13188
  target: "_blank",
13065
13189
  rel: "noopener noreferrer",
13066
13190
  "access-cta": "DIY",
@@ -13078,23 +13202,53 @@ function StickyCta({
13078
13202
  size: iconSize,
13079
13203
  color: iconColor,
13080
13204
  iconImageUrl: p.iconImageUrl
13081
- }), /*#__PURE__*/React.createElement("span", null, p.whatsAppLabel)));
13205
+ }), p.whatsAppLabel ? /*#__PURE__*/React.createElement("span", {
13206
+ dangerouslySetInnerHTML: {
13207
+ __html: p.whatsAppLabel
13208
+ }
13209
+ }) : null));
13082
13210
  }
13083
13211
 
13084
13212
  // type === 'sticky' – use primary Button (same as other sections) linking to WhatsApp
13085
13213
  const barStyle = getStickyBarStyle(breakpoint, theme);
13086
13214
  const textStyle = getStickyTextStyle(breakpoint, theme);
13087
- return /*#__PURE__*/React.createElement(React.Fragment, null, isMobile ? null : editHintBlock, /*#__PURE__*/React.createElement("div", {
13215
+ useEffect(() => {
13216
+ document.body.classList.add('has-sticky-cta');
13217
+ return () => document.body.classList.remove('has-sticky-cta');
13218
+ }, []);
13219
+ const stickyBarPaddingStyles = `
13220
+ body.has-sticky-cta {
13221
+ padding-bottom: ${STICKY_BAR_HEIGHT_BY_BREAKPOINT.mobile}px;
13222
+ }
13223
+ @media (min-width: ${BREAKPOINTS.tablet}px) {
13224
+ body.has-sticky-cta {
13225
+ padding-bottom: ${STICKY_BAR_HEIGHT_BY_BREAKPOINT.tablet}px;
13226
+ }
13227
+ }
13228
+ @media (min-width: ${BREAKPOINTS.desktop}px) {
13229
+ body.has-sticky-cta {
13230
+ padding-bottom: ${STICKY_BAR_HEIGHT_BY_BREAKPOINT.desktop}px;
13231
+ }
13232
+ }
13233
+ `;
13234
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("style", {
13235
+ dangerouslySetInnerHTML: {
13236
+ __html: stickyBarPaddingStyles
13237
+ }
13238
+ }), isMobile ? null : editHintBlock, /*#__PURE__*/React.createElement("div", {
13088
13239
  style: barStyle,
13089
13240
  role: "banner",
13090
13241
  "access-cta-sticky": "DIY"
13091
13242
  }, /*#__PURE__*/React.createElement("p", {
13092
- style: textStyle
13093
- }, p.stickyMessage), /*#__PURE__*/React.createElement(Button, {
13243
+ style: textStyle,
13244
+ dangerouslySetInnerHTML: {
13245
+ __html: p.stickyMessage || ''
13246
+ }
13247
+ }), /*#__PURE__*/React.createElement(Button, {
13094
13248
  data: {
13095
13249
  isLink: 1,
13096
13250
  isExternal: 1,
13097
- link: whatsAppUrl,
13251
+ link: redirectUrl,
13098
13252
  value: p.stickyButtonText
13099
13253
  },
13100
13254
  type: "primary",
@@ -13658,7 +13812,13 @@ function VideoWorkshopPromotion({
13658
13812
  ref: videoData?.videoTextContent?.metadata?.refSetter,
13659
13813
  data: videoData?.videoTextContent?.metadata,
13660
13814
  type: 'primary',
13661
- size: isMobile ? 'small' : 'medium'
13815
+ size: isMobile ? 'small' : 'medium',
13816
+ onClick: () => {
13817
+ const url = videoData?.videoTextHeading?.metadata?.value;
13818
+ if (url) {
13819
+ window.open(url, '_blank', 'noopener,noreferrer');
13820
+ }
13821
+ }
13662
13822
  }))), /*#__PURE__*/React.createElement("div", {
13663
13823
  className: classes.videoBlock
13664
13824
  }, /*#__PURE__*/React.createElement("div", {