PageSourceSearch

https://www.motilaloswalmf.com/scripts/constant.js

js motilaloswalmf.com collected 2026-10-01 15:06:54 UTC 29,292 bytes, 767 lines download raw bytes

1const dataMapMoObj = {
2  ObjTemp: {
3    inception_Ret: 'Since Inception',
4    oneYear_Ret: '1 Year',
5    threeYear_Ret: '3 Years',
6    fiveYear_Ret: '5 Years',
7    sevenYear_Ret: '7 Years',
8    tenYear_Ret: '10 Years',
9    'Since Inception': 'inception_Ret',
10    '1 Year': 'oneYear_Ret',
11    '3 Years': 'threeYear_Ret',
12    '5 Years': 'fiveYear_Ret',
13    '7 Years': 'sevenYear_Ret',
14    '10 Years': 'tenYear_Ret',
15  },
16  ObjTempfdp: {
17    inception_Ret: 'Since Inception',
18    oneYear_Ret: 'Since 1 year',
19    threeYear_Ret: 'Since 3 years',
20    fiveYear_Ret: 'Since 5 years',
21    sevenYear_Ret: 'Since 7 years',
22    tenYear_Ret: 'Since 10 years',
23    'Since Inception': 'inception_Ret',
24    'Since 1 year': 'oneYear_Ret',
25    'Since 3 years': 'threeYear_Ret',
26    'Since 5 years': 'fiveYear_Ret',
27    'Since 7 years': 'sevenYear_Ret',
28    'Since 10 years': 'tenYear_Ret',
29  },
30  objtempdrop: {
31    'Since Inception': 'Since Inception',
32    '1 year': '1 Year',
33    '3 years': '3 Years',
34    '5 years': '5 Years',
35    '7 years': '7 Years',
36    '10 years': '10 Years',
37  },
38  icons: {
39    'very-high': 'very-high-risk',
40    'low-to-moderate': 'moderately-low-risk',
41    high: 'high-risk',
42    moderate: 'moderate-risk',
43    low: 'low-risk',
44  },
45  iconsNfo: {
46    'very-high': 'very-high-risk',
47    'low-to-moderate': 'moderately-low-risk',
48    high: 'high-risk',
49    moderate: 'moderate-risk',
50    low: 'low-risk',
51  },
52  selectreturns: '3 Years',
53  schmenmob: 'Popular',
54  schstar: [],
55  tempMobReturn: [],
56  selectreturnstemp: '',
57  gropcodevalue: '',
58  fundManagerDetails: '',
59  selectviewFunds: '',
60  deskrightdrp: '',
61  CLASS_PREFIXES: ['block-item', 'block-subitem', 'block-subitem-finelsub'],
62  addIndexed(parentElement, level = 0) {
63    if (level >= this.CLASS_PREFIXES.length || !parentElement.children.length) {
64      return;
65    }
66    const prefix = this.CLASS_PREFIXES[level];
67    const { children } = parentElement; // Cache children for clarity.
68    for (let i = 0; i < children.length; i += 1) {
69      const child = children[i];
70      const index = i + 1; // Class names are typically 1-based.
71      child.classList.add('comlist');
72      child.classList.add(`${prefix}${index}`);
73      this.addIndexed(child, level + 1);
74    }
75  },
76  addIndexedTwo(parentElement, level = 0) {
77    if (level >= this.CLASS_PREFIXES.length || !parentElement.children.length) {
78      return;
79    }
80    const prefix = this.CLASS_PREFIXES[level];
81    const { children } = parentElement; // Cache children for clarity.
82    for (let i = 0; i < children.length; i += 1) {
83      let parClass = Array.from(children[0].parentElement.classList)[0].split('-').at(-2);
84      const child = children[i];
85      const index = i + 1; // Class names are typically 1-based.
86      child.classList.add(`${prefix}${parClass}${index}`);
87      parClass = '';
88      this.addIndexedTwo(child, level + 1);
89    }
90  },
91  altFunction: (element, altTextr) => {
92    if (!element) return;
93    element.setAttribute('alt', altTextr);
94  },
95  myAPI: async (method, url, body = null) => {
96    const options = { method };
97    if (body) {
98      options.headers = { 'Content-Type': 'application/json' };
99      options.body = JSON.stringify(body);
100    }
101    const response = await fetch(url, options);
102    if (!response.ok) throw new Error(`Request failed: ${response.status}`);
103    const text = await response.text();
104    try {
105      return JSON.parse(text);
106    } catch (e) {
107      return text;
108    }
109  },
110  formatDate: (dateString) => {
111    const date = new Date(`${dateString}T00:00:00`);
112
113    // Array of month abbreviations
114    const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
115
116    const day = date.getDate();
117    const month = months[date.getMonth()];
118    const year = date.getFullYear();
119
120    // Function to get the correct ordinal suffix (st, nd, rd, th)
121    // function getOrdinalSuffix(days) {
122    //   if (days > 3 && days < 21) return 'th'; // for teens
123    //   switch (days % 10) {
124    //     case 1: return 'st';
125    //     case 2: return 'nd';
126    //     case 3: return 'rd';
127    //     default: return 'th';
128    //   }
129    // }
130
131    const dayWithSuffix = day; // + getOrdinalSuffix(day);
132
133    // Combine parts into the final format
134    return `${dayWithSuffix} ${month} ${year}`;
135  },
136  toTitleCase: (str) => {
137    if (!str) {
138      return '';
139    }
140
141    return str
142      .toLowerCase()
143      .split(' ')
144      .map((word) => {
145        if (word.length === 0) {
146          return '';
147        }
148        return word.charAt(0).toUpperCase() + word.slice(1);
149      })
150      .join(' ');
151  },
152  setupPagination: (blockdiv, items, itemsPerPage) => {
153    const funcObj = {};
154    const totalPages = Math.ceil(items.length / itemsPerPage);
155    // 1. If 1 or 0 pages, do nothing
156    if (totalPages <= 1) return;
157    let currentPage = 1;
158    const mainContainer = blockdiv.closest('.section');
159    if (!mainContainer) return;
160    let ptag = null;
161    let targetAppendPoint = mainContainer;
162    // --- WRAPPER LOGIC FOR DOWNLOADSEC ---
163    if (mainContainer.getAttribute('data-id') === 'downloadsec') {
164      const filteredContent = blockdiv.querySelector('.download-container .filtered-content');
165      if (filteredContent) {
166        // Create the combined wrapper for results + pagination
167        const footerWrapper = document.createElement('div');
168        footerWrapper.classList.add('download-pagination-footer');
169        // Create Results Section (the p tag block)
170        const downmainContainer = document.createElement('div');
171        downmainContainer.classList.add('download-result');
172        ptag = document.createElement('p');
173        ptag.classList.add('pagination-info');
174        downmainContainer.appendChild(ptag);
175        footerWrapper.appendChild(downmainContainer);
176        // Set the target where pagination buttons will be added
177        targetAppendPoint = footerWrapper;
178        filteredContent.appendChild(footerWrapper);
179      }
180    }
181    const paginationNav = document.createElement('nav');
182    paginationNav.className = 'pagination-nav';
183    paginationNav.setAttribute('aria-label', 'Pagination');
184    const paginationWrapper = document.createElement('ul');
185    paginationWrapper.className = 'pagination-wrapper';
186    paginationNav.appendChild(paginationWrapper);
187    // --- CORE LOGIC: Smart Gaps ---
188    function generatePaginationList(current, total) {
189      if (total <= 6) return Array.from({ length: total }, (_, i) => i + 1);
190      const center = [current - 1, current, current + 1].filter((p) => p > 1 && p < total);
191      const filteredList = [...new Set([1, ...center, total])].sort((a, b) => a - b);
192      const finalParams = [];
193      let prev = null;
194      filteredList.forEach((page) => {
195        if (prev) {
196          const gap = page - prev;
197          if (gap === 2) finalParams.push(prev + 1);
198          else if (gap > 2) finalParams.push('...');
199        }
200        finalParams.push(page);
201        prev = page;
202      });
203      return finalParams;
204    }
205    // --- CONTROLLER ---
206    function goToPage(page, shouldScroll = true) {
207      const banner = document.querySelector('.section.fdp-campaign-container');
208      if (banner) {
209        banner.classList.toggle('dsp-none', page > 1);
210      }
211      // document.querySelector()
212      currentPage = page;
213      const start = (page - 1) * itemsPerPage;
214      const end = Math.min(start + itemsPerPage, items.length);
215      // Update the result text inside the footer
216      if (ptag) {
217        ptag.innerText = `Showing ${start + 1}-${end} of ${items.length} results`;
218      }
219      // Toggle visibility of items
220      items.forEach((item, index) => {
221        if (index < start || index >= end) {
222          item.classList.add('hidden-item');
223        } else {
224          item.classList.remove('hidden-item');
225        }
226      });
227      funcObj.renderControls();
228      // Scroll back to the top of the block
229      // blockdiv.scrollIntoView({ behavior: 'smooth', block: 'start' });
230      // if (shouldScroll) {
231      //   blockdiv.scrollIntoView({ behavior: 'smooth', block: 'start' });
232      // }
233      if (shouldScroll) {
234        const offset = 0; // Distance from the top you want to keep
235        const bodyRect = document.head.getBoundingClientRect().top;
236        const elementRect = blockdiv.getBoundingClientRect().top;
237        const elementPosition = elementRect - bodyRect;
238        const offsetPosition = elementPosition - offset;
239
240        window.scrollTo({
241          top: offsetPosition,
242          behavior: 'smooth',
243        });
244      }
245    }
246    // --- RENDERER ---
247    const renderControls = () => {
248      paginationWrapper.innerHTML = '';
249      const prevItem = document.createElement('li');
250      const prevBtn = document.createElement('button');
251      prevBtn.innerHTML = '<span aria-hidden="true">&lsaquo;</span><span class="sr-only">Previous</span>';
252      prevBtn.className = 'pagination-arrow prev-btn';
253      prevBtn.disabled = currentPage === 1;
254      prevBtn.onclick = () => { if (currentPage > 1) goToPage(currentPage - 1); };
255      prevItem.appendChild(prevBtn);
256      paginationWrapper.appendChild(prevItem);
257      generatePaginationList(currentPage, totalPages).forEach((item) => {
258        if (item === '...') {
259          const ellipsisItem = document.createElement('li');
260          const ellipsis = document.createElement('span');
261          ellipsis.textContent = '...';
262          ellipsis.className = 'pagination-ellipsis';
263          ellipsisItem.appendChild(ellipsis);
264          paginationWrapper.appendChild(ellipsisItem);
265        } else {
266          const pageItem = document.createElement('li');
267          const btn = document.createElement('button');
268          btn.textContent = item;
269          btn.className = `pagination-btn${item === currentPage ? ' active' : ''}`;
270          if (item === currentPage) {
271            const selectedText = document.createElement('span');
272            selectedText.className = 'sr-only';
273            selectedText.textContent = ' selected';
274            btn.appendChild(selectedText);
275          }
276          btn.onclick = () => goToPage(item);
277          pageItem.appendChild(btn);
278          paginationWrapper.appendChild(pageItem);
279        }
280      });
281      const nextItem = document.createElement('li');
282      const nextBtn = document.createElement('button');
283      nextBtn.innerHTML = '<span aria-hidden="true">&rsaquo;</span><span class="sr-only">Next</span>';
284      nextBtn.className = 'pagination-arrow next-btn';
285      nextBtn.disabled = currentPage === totalPages;
286      nextBtn.onclick = () => { if (currentPage < totalPages) goToPage(currentPage + 1); };
287      nextItem.appendChild(nextBtn);
288      paginationWrapper.appendChild(nextItem);
289    };
290    funcObj.renderControls = renderControls;
291    // --- INITIALIZE ---
292    // Appends the paginationWrapper inside the shared footerWrapper
293    targetAppendPoint.appendChild(paginationNav);
294    goToPage(1, false);
295  },
296  getOrdinalSuperscript: (n) => {
297    // Ensure the input is a number
298    if (typeof n !== 'number' || Number.isNaN(n)) {
299      return String(n); // Return the input as is if it's not a valid number
300    }
301
302    // Handle the special cases for 11, 12, and 13 (which always use 'th')
303    const s = n % 100;
304    if (s >= 11 && s <= 13) {
305      return `${n}<sup>th</sup>`;
306    }
307
308    // Handle the common cases using the last digit
309    const lastDigit = n % 10;
310    let suffix = '';
311
312    switch (lastDigit) {
313      case 1:
314        suffix = 'st';
315        break;
316      case 2:
317        suffix = 'nd';
318        break;
319      case 3:
320        suffix = 'rd';
321        break;
322      default:
323        suffix = 'th';
324        break;
325    }
326
327    // Return the number concatenated with the superscript suffix
328    return `${n}<sup>${suffix}</sup>`;
329  },
330  getReadingTime: async (currentPath) => {
331    try {
332      // 1. Fetch the HTML
333      const url = `${currentPath}`;
334      const resp = await fetch(url);
335      if (!resp.ok) throw new Error('Network response was not ok');
336      const html = await resp.text();
337
338      // 2. Parse HTML into a Document Object
339      const parser = new DOMParser();
340      const doc = parser.parseFromString(html, 'text/html');
341      let videotag = '';
342      if (doc.querySelector('.embed a')) {
343        videotag = 'video';
344      }
345      let imgbanner;
346      if (doc.querySelector('body .embed picture img')) {
347        imgbanner = doc.querySelector('body .embed picture img')
348          .getAttribute('src');
349      } else if (doc.querySelector('body .embed picture img') === null) {
350        imgbanner = './media_17ae039b1fa04e49d0fc8d1afef9f86f7f39f7c25.png?width=1200&format=pjpg&optimize=medium';
351        // doc.querySelector('body .moedge-article-video picture img').getAttribute('src');
352      }
353      // 3. Select the content
354      // Replace '.article-content' with the actual class name of your text container
355      // for better accuracy (e.g., 'main', 'article', '#post-body').
356      const contentEl = doc.querySelector('article')
357        || doc.querySelector('.article-body')
358        || doc.querySelector('main')
359        || doc.body;
360
361      // 4. Extract text and count words
362      const text = contentEl.innerText || contentEl.textContent;
363      // Split by whitespace and filter out empty strings
364      const wordCount = text.trim().split(/\s+/).filter((word) => word.length > 0).length;
365
366      // 5. Calculate Reading Time
367      // Standard speed is 200 words per minute (WPM)
368      const wpm = 200;
369      const minutes = Math.ceil(wordCount / wpm);
370      const titlebodyPage = doc.querySelector('h1') !== null ? doc.querySelector('h1').textContent.trim() : '';
371      const discription = doc.querySelector('.co-branding') !== null ? doc.querySelector('.co-branding').textContent.trim() : '';
372      const signaldisp = doc.querySelectorAll('div:nth-child(8) p');
373      const signalarr = [];
374      signaldisp?.forEach((item) => {
375        if (item.textContent.trim() !== '' && item.textContent.trim().split(' ').length > 6) {
376          signalarr.push(item.textContent.trim());
377        }
378      });
379      return {
380        minutes,
381        wordCount,
382        text: `${minutes}`,
383        imgbanner,
384        videotag,
385        titlebodyPage,
386        discription,
387        signalarr,
388      };
389    } catch (error) {
390      console.error('Failed to calculate reading time:', error);
391      return { minutes: 0, wordCount: 0, text: '' };
392    }
393  },
394  toggleLoaderSpinner: (element, isActive) => {
395    const parseRgbValues = (colorValue) => {
396      if (!colorValue) return null;
397      const rgbMatch = colorValue.match(/rgba?\(([^)]+)\)/i);
398      if (!rgbMatch) return null;
399
400      const parts = rgbMatch[1].split(',').map((p) => p.trim());
401      if (parts.length < 3) return null;
402
403      const r = Number(parts[0]);
404      const g = Number(parts[1]);
405      const b = Number(parts[2]);
406      const a = parts.length > 3 ? Number(parts[3]) : 1;
407
408      if ([r, g, b].some((n) => Number.isNaN(n))) return null;
409      if (Number.isNaN(a)) return { r, g, b, a: 1 };
410      return { r, g, b, a };
411    };
412
413    const resolveEffectiveBackground = (startEl) => {
414      let node = startEl;
415      while (node && node !== document.documentElement) {
416        const bg = window.getComputedStyle(node).backgroundColor;
417        const parsed = parseRgbValues(bg);
418        if (parsed && parsed.a > 0) return parsed;
419        node = node.parentElement;
420      }
421
422      const rootBg = window.getComputedStyle(document.body).backgroundColor;
423      return parseRgbValues(rootBg) || { r: 255, g: 255, b: 255, a: 1 };
424    };
425
426    const getPerceivedBrightness = (rgb) => (
427      (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000
428    );
429
430    const effectiveBg = resolveEffectiveBackground(element);
431    const useWhiteDots = getPerceivedBrightness(effectiveBg) < 128;
432
433    const loadSpinn = element.querySelector('.loader-spinner');
434    if (loadSpinn === null) {
435      dataMapMoObj.loaderDiv = document.createElement('div');
436      dataMapMoObj.loaderDiv.className = 'loader-spinner';
437      if (useWhiteDots) {
438        dataMapMoObj.loaderDiv.classList.add('is-white');
439      }
440
441      const spinner = document.createElement('div');
442      spinner.className = 'spinner';
443      spinner.setAttribute('role', 'img');
444      spinner.setAttribute('aria-label', 'Loading');
445
446      for (let i = 0; i < 8; i += 1) {
447        const dot = document.createElement('span');
448        dot.className = 'dot';
449        spinner.appendChild(dot);
450      }
451
452      dataMapMoObj.loaderDiv.appendChild(spinner);
453      element.appendChild(dataMapMoObj.loaderDiv);
454      element.style.position = 'relative';
455    } else {
456      loadSpinn.classList.toggle('is-white', useWhiteDots);
457      dataMapMoObj.loaderDiv = loadSpinn;
458    }
459    if (isActive) {
460      dataMapMoObj.loaderDiv.classList.add('loader-active');
461    } else {
462      dataMapMoObj.loaderDiv.classList.remove('loader-active');
463      // dataMapMoObj.loaderDiv.remove();
464    }
465  },
466  htmlListToJSON: (rootElement) => {
467    // --- STAGE 1: HTML to JSON ---
468    // Reads an element's text while preserving <br> tags as newlines. Using
469    // textContent directly would collapse "A<br>B" into "AB" (losing the line
470    // break); this keeps it as "A\nB" so consumers can split on the newline.
471    const textWithBreaks = (el) => {
472      if (!el) return '';
473      if (!el.querySelector || !el.querySelector('br')) {
474        return el.textContent.trim();
475      }
476      const clone = el.cloneNode(true);
477      clone.querySelectorAll('br').forEach((br) => br.replaceWith('\n'));
478      return clone.textContent.trim();
479    };
480    const htmlToJSON = (element) => {
481      const result = {};
482      const listItems = element.querySelectorAll(':scope > li');
483
484      if (listItems.length === 0) return element.textContent.trim();
485
486      listItems.forEach((li, index) => {
487        const keyElement = li.querySelector(':scope > strong, :scope > p > strong');
488        const nestedUl = li.querySelector(':scope > ul, :scope > ol');
489
490        if (keyElement) {
491          const key = keyElement.textContent.replace(':', '').trim();
492
493          if (nestedUl) {
494            const subData = htmlToJSON(nestedUl);
495            // Captures paragraphs like "This product is suitable for..."
496            const descPara = li.querySelector(':scope > p:not(:has(strong))');
497            const description = descPara ? descPara.textContent.trim() : null;
498
499            result[key] = description ? { description, items: subData } : subData;
500          } else {
501            let value = li.textContent.replace(keyElement.textContent, '').trim();
502            if (value !== '' && !isNaN(value)) value = Number(value);
503            result[key] = value;
504          }
505        } else {
506          // Handle plain items or nested lists without a strong key.
507          // Use textWithBreaks so a <br> inside the <li> is preserved as a
508          // newline (e.g. "Day 7 onwards - Nil<br>Exit Load ..." -> two lines).
509          result[index] = nestedUl ? htmlToJSON(nestedUl) : textWithBreaks(li);
510        }
511      });
512
513      // Convert numeric-key objects to Arrays
514      const keys = Object.keys(result);
515      if (keys.length > 0 && keys.every((k) => !isNaN(k))) {
516        return Object.values(result);
517      }
518      return result;
519    };
520
521    // --- STAGE 2: Clean Metadata Strings ---
522    const cleanMetadata = (data) => {
523      if (typeof data !== 'object' || data === null) return data;
524
525      // Pattern for metadata: contains newlines and colons
526      const isMeta = (val) => typeof val === 'string' && val.includes('\n') && val.includes(':');
527
528      const parseMeta = (str) => {
529        const obj = {};
530        str.split('\n').forEach((line) => {
531          const trimmed = line.trim();
532          if (trimmed.includes(':')) {
533            const [k, ...v] = trimmed.split(':');
534            let val = v.join(':').trim();
535            if (val !== '' && !isNaN(val)) val = Number(val);
536            obj[k.trim()] = val;
537          }
538        });
539        return obj;
540      };
541
542      // Recursively walk the JSON to find strings to parse
543      for (const key in data) {
544        if (isMeta(data[key]) && key !== 'exitLoad') { // Skip exitLoad to keep as text
545          data[key] = parseMeta(data[key]);
546        } else if (typeof data[key] === 'object') {
547          data[key] = cleanMetadata(data[key]);
548        }
549      }
550      return data;
551    };
552
553    const initialJson = htmlToJSON(rootElement);
554    return cleanMetadata(initialJson);
555  },
556
557};
558export default dataMapMoObj;
559
560export const mo_base_url = {
561  uat: 'https://mf.moamc.com/v2',
562  prod: 'https://invest.motilaloswalmf.com',
563};
564
565export const validationRegex = {
566  email: /^(?=.{3,30}@)[a-z0-9._#$-]+@[a-z0-9-]+(\.[a-z0-9-]+)+$/i,
567}
568
569/**
570 * Formats a date string to "dd MMM yyyy" (e.g., "18 Feb 2025")
571 * Accepts: "yyyy-MM-dd", "dd-MM-yyyy", "dd MMM yyyy"
572 */
573export function formatDateToDDMMMYY(dateStr) {
574  if (!dateStr) return '';
575  const str = String(dateStr).trim();
576  const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
577
578  let day, month, year;
579
580  if (/^\d{4}-\d{2}-\d{2}$/.test(str)) {
581    [year, month, day] = str.split('-');
582  } else if (/^\d{1,2}-\d{1,2}-\d{4}$/.test(str)) {
583    [day, month, year] = str.split('-');
584  } else {
585    const d = new Date(str);
586    if (!Number.isNaN(d.getTime())) {
587      day = String(d.getDate()).padStart(2, '0');
588      month = String(d.getMonth() + 1);
589      year = String(d.getFullYear());
590    } else {
591      return str;
592    }
593  }
594
595  return `${Number(day).toString().padStart(2, '0')} ${monthNames[parseInt(month, 10) - 1]} ${year}`;
596}
597
598/**
599 * Formats a number with Indian comma separation (lakh/crore style)
600 * e.g., 400036.02 → "4,00,036.02"
601 */
602export function formatAmountIndian(amount) {
603  if (amount === null || amount === undefined || isNaN(amount)) return '';
604  const fixed = Number(amount).toFixed(2);
605  return Number(fixed).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
606}
607
608/**
609 * Central registry of NFO / index funds and their static handling.
610 * This is the single source of truth for per-NFO behaviour that used to be
611 * hardcoded across blocks (tracking-API skips, reopen dates, banner links).
612 *
613 * ── HOW TO ADD A NEW NFO ──────────────────────────────────────────────
614 * When a new NFO launches, just add one entry below. No block code changes
615 * are needed — the blocks read from this list via the helpers.
616 *
617 * Fields (all optional — provide whichever apply):
618 *   schcode          Scheme code, e.g. 'QF'. Used by tracking-API skip
619 *                    (key-facts.js, tabs.js), fund-card.js and fdp-card.js.
620 *   slug             Page URL slug, e.g. 'motilal-oswal-...-fund'. Used to
621 *                    match the fund by pathname for the tracking-API skip.
622 *   fundName         Authored fund name, e.g. 'Motilal Oswal Quality Fund'.
623 *                    Used by the header NFO banner (header.js).
624 *   reopenDate       NFO reopen date, e.g. '28-Sep-2026'. Used by the invest
625 *                    date pickers (invest-now-homepage.js), fund-card.js,
626 *                    fdp-card.js.
627 *   sipFirstInstallmentDate  Optional. Used ONLY for the SIP date-picker
628 *                    selection (the default / minimum date shown to the user)
629 *                    in invest-now-homepage.js. All other NFO logic still
630 *                    depends on `reopenDate`. When not set, the picker falls
631 *                    back to `reopenDate`.
632 *   investPath       Relative invest link (path + query) for the header NFO
633 *                    banner. The domain is resolved per environment
634 *                    (dev / stage / prod) at runtime via buildInvestUrl().
635 *   investUrl        Fully-qualified invest link (fallback if investPath is
636 *                    not set). Prefer investPath so the URL stays env-aware.
637 *   homepageInvestNfo Set true only for funds whose reopen date should drive
638 *                    the invest-now-homepage date picker.
639 */
640export const NFO_FUNDS = [
641  {
642    schcode: 'RI',
643    slug: 'motilal-oswal-nifty-reits-and-realty-index-fund',
644    fundName: 'Motilal Oswal Nifty REITs and Realty Index Fund',
645    reopenDate: '22-Oct-2026',
646    // Used ONLY for the SIP date-picker selection (default / min date shown to
647    // the user). All other NFO logic still depends on reopenDate (22-Oct).
648    sipFirstInstallmentDate: '26-Oct-2026',
649    // Only the fund path + query is stored; the domain is resolved at runtime
650    // per environment (dev / stage / prod) — see buildInvestUrl().
651    investPath: '/mutual-funds/motilal-oswal-nifty-reits-and-realty-index-fund?utm_source=Desktop&utm_medium=homepage_header&utm_campaign=nifty-reits-and-realty-fund',
652    // Handled by the invest-now-homepage date picker (forces NFO reopen date).
653    homepageInvestNfo: true,
654  },
655  {
656    // Quality Fund is still within its NFO/closed window (reopens 24-Sep-2026).
657    // Kept only to force the invest-now-homepage SIP date picker to the reopen
658    // date until then. Can be removed once QF trades as a normal open-ended fund.
659    schcode: 'QF',
660    reopenDate: '22-Sep-2026',
661  },
662];
663
664/**
665 * Environment-aware base URL for the public marketing site (motilaloswalmf.com).
666 * Prod hosts (www / beta / stage) resolve to the live domain; everything else
667 * (dev, hlx/aem previews, localhost) resolves to the dev domain — matching the
668 * detection pattern already used across the codebase.
669 * @returns {string} origin without trailing slash, e.g. 'https://www.motilaloswalmf.com'
670 */
671export function getInvestSiteBaseUrl() {
672  const origin = (typeof window !== 'undefined' && window.location?.origin) || '';
673  const isProd = origin.includes('www.motilaloswalmf')
674    || origin.includes('beta.motilaloswalmf')
675    || origin.includes('stage.');
676  return isProd
677    ? 'https://www.motilaloswalmf.com'
678    : 'https://dev.motilaloswalmf.com';
679}
680
681/**
682 * Build the full, environment-aware invest URL for an NFO fund entry.
683 * Prefers the relative `investPath` (domain resolved per env). Falls back to a
684 * fully-qualified `investUrl` if one is authored, then to undefined.
685 * @param {object} fund - an entry from NFO_FUNDS.
686 * @returns {string|undefined}
687 */
688export function buildInvestUrl(fund) {
689  if (!fund) return undefined;
690  if (fund.investPath) {
691    return `${getInvestSiteBaseUrl()}${fund.investPath}`;
692  }
693  return fund.investUrl;
694}
695
696/**
697 * Look up an NFO fund config entry by scheme code (case-insensitive).
698 * @param {string} schcode
699 * @returns {object|undefined}
700 */
701export function getNfoFundBySchcode(schcode) {
702  if (!schcode) return undefined;
703  const code = String(schcode).toUpperCase();
704  return NFO_FUNDS.find((fund) => fund.schcode && fund.schcode.toUpperCase() === code);
705}
706
707/**
708 * Look up an NFO fund config entry by authored fund name (case-insensitive).
709 * @param {string} fundName
710 * @returns {object|undefined}
711 */
712export function getNfoFundByName(fundName) {
713  if (!fundName) return undefined;
714  // Normalise so the banner text matches regardless of how it is authored
715  // (e.g. "REITs & Realty" vs "REITs and Realty", or extra whitespace).
716  const normalise = (value) => String(value)
717    .toLowerCase()
718    .replace(/&/g, 'and')
719    .replace(/\s+/g, ' ')
720    .trim();
721  const name = normalise(fundName);
722  return NFO_FUNDS.find((fund) => fund.fundName && normalise(fund.fundName) === name);
723}
724
725/**
726 * Returns true while a fund's NFO is still active (i.e. its reopen date has not
727 * yet passed). Once the reopen date is in the past, the fund trades normally so
728 * the tracking-error / tracking-difference APIs SHOULD be called again.
729 * A fund entry with no `reopenDate` is treated as always active.
730 * @param {object} fund - an entry from NFO_FUNDS.
731 * @returns {boolean}
732 */
733export function isNfoActive(fund) {
734  if (!fund) return false;
735  if (!fund.reopenDate) return true;
736  const reopen = new Date(fund.reopenDate);
737  if (Number.isNaN(reopen.getTime())) return true;
738  // Active until the end of the reopen day.
739  reopen.setHours(23, 59, 59, 999);
740  return reopen.getTime() >= Date.now();
741}
742
743/**
744 * Returns true when the current fund is an NFO/index fund that should skip
745 * the tracking-error / tracking-difference API calls.
746 * Matches on plan code, plan label, resolved scheme code, or the page URL, and
747 * only while the NFO is still active — after the reopen date passes, the
748 * tracking APIs are called again.
749 *
750 * @param {object} [dataObj=dataMapMoObj] - the shared data map object.
751 * @param {string} [planCode] - optional plan/scheme code (e.g. from dataObj.schemecode).
752 * @param {string} [planLabel] - optional plan label.
753 * @returns {boolean}
754 */
755export function isNfoFund(dataObj = dataMapMoObj, planCode, planLabel) {
756  const resolvedSchcode = dataObj?.schemeCodeResp?.[0]?.schcode;
757  const codes = [planCode, planLabel, resolvedSchcode, dataObj?.schemecode]
758    .filter(Boolean)
759    .map((c) => String(c).toUpperCase());
760  const pathname = (typeof window !== 'undefined' && window.location?.pathname) || '';
761
762  return NFO_FUNDS.some((fund) => {
763    const codeMatch = fund.schcode && codes.includes(fund.schcode.toUpperCase());
764    const slugMatch = fund.slug && pathname.includes(fund.slug);
765    return (codeMatch || slugMatch) && isNfoActive(fund);
766  });
767}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.