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">‹</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">›</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.