My Cart
Added to cart
/* ================================================================ IVYKISS LANDING PAGE — Custom Scripts 적용 위치: Odoo > Edit > Code Injection > ================================================================ */ (function () { 'use strict'; /* ── CONFIG ─────────────────────────────────────────────── */ const DEAL_END_DATE = new Date('2026-05-31T23:59:59'); /* Limited Time Deals tab definitions — IDs match actual #wrap sibling sections */ const LTD_TABS = [ { label: 'All', id: 'bs-all' }, { label: 'IVY', id: 'bs-ivy' }, { label: 'RED', id: 'bs-red' }, { label: 'Vivace', id: 'bs-vc' }, { label: '메스틱', id: 'bs-acc' }, ]; /* ── HELPERS ────────────────────────────────────────────── */ function mkEl(tag, opts) { const el = document.createElement(tag); if (opts) { if (opts.cls) el.className = opts.cls; if (opts.text != null) el.textContent = opts.text; if (opts.html != null) el.innerHTML = opts.html; if (opts.href) el.setAttribute('href', opts.href); } return el; } function chevronSvg(dir) { const d = dir === 'left' ? 'M15 18L9 12l6-6' : 'M9 18l6-6-6-6'; return ` `; } /* Figma 16353:45923 — days number (red-orange) + "Days Left" label */ function startCountdown(el, endDate) { function tick() { const diff = endDate - Date.now(); const d = diff > 0 ? Math.floor(diff / 86400000) : 0; el.innerHTML = `${d}` + `Days Left`; } tick(); setInterval(tick, 1000); } /* ================================================================ 1. LIMITED TIME DEALS Structure: section (heading) + sibling sections (tab panels) Panels are #wrap siblings, not children of the heading section. Show/hide is via inline style (display + visibility). ================================================================ */ function setupLimitedTimeDeals() { const wrap = document.getElementById('wrap'); if (!wrap) return; const sections = Array.from(wrap.children); /* Find the heading section by its h1 text */ const ltdIdx = sections.findIndex(el => { const h = el.querySelector('h1'); return h && h.textContent.trim() === 'Limited Time Deals'; }); if (ltdIdx < 0) return; const ltdSec = sections[ltdIdx]; if (ltdSec.dataset.ivyLtd) return; /* idempotent */ ltdSec.dataset.ivyLtd = '1'; /* Collect sibling tab panels — consecutive s_dynamic_snippet sections immediately after the heading with IDs matching LTD_TABS */ const panelIdSet = new Set(LTD_TABS.map(t => t.id)); const tabPanels = []; for (let i = ltdIdx + 1; i < Math.min(ltdIdx + 8, sections.length); i++) { const s = sections[i]; if (s.classList.contains('s_dynamic_snippet_prod_temp') && panelIdSet.has(s.id)) { tabPanels.push(s); } else { break; } } const container = ltdSec.querySelector('.container'); if (!container) return; /* 1a) Hide the heading section's own carousel — panels are in siblings */ const ownCarousel = container.querySelector('.dynamic_snippet_template'); if (ownCarousel) ownCarousel.style.display = 'none'; /* 1b) Modify title group: add ivy-ltd-title-group class, hide the "This Month only" badge span, append countdown */ const titleGroup = container.querySelector('.d-flex.align-items-center.gap-3'); if (titleGroup && !titleGroup.dataset.ivyMod) { titleGroup.dataset.ivyMod = '1'; titleGroup.classList.add('ivy-ltd-title-group'); /* Hide "This Month only" span (first span child) */ const badge = titleGroup.querySelector('span'); if (badge) badge.style.display = 'none'; /* Append countdown */ const cd = mkEl('span', { cls: 'ivy-ltd-countdown' }); titleGroup.appendChild(cd); startCountdown(cd, DEAL_END_DATE); } /* Panel show/hide — must strip the animation-system inline constraints (height:0; overflow:hidden; opacity:0) that Odoo sets on hidden panels */ function showPanel(p) { p.style.removeProperty('height'); p.style.removeProperty('overflow'); p.style.setProperty('opacity', '1', 'important'); p.style.setProperty('padding-top', '40px', 'important'); p.style.setProperty('padding-bottom', '40px', 'important'); p.style.display = 'block'; p.style.visibility = 'visible'; p.classList.add('ivy-active'); } function hidePanel(p) { p.style.setProperty('opacity', '0', 'important'); p.style.setProperty('height', '0px', 'important'); p.style.setProperty('overflow', 'hidden', 'important'); p.style.setProperty('padding-top', '0px', 'important'); p.style.setProperty('padding-bottom', '0px', 'important'); p.style.display = 'none'; p.style.visibility = 'hidden'; p.classList.remove('ivy-active'); } /* 1c) Insert tab bar after the header row */ const headerRow = container.querySelector('.d-flex.align-items-center.justify-content-between'); if (headerRow && !container.querySelector('.ivy-tabs')) { const tabBar = mkEl('div', { cls: 'ivy-tabs' }); tabBar.style.marginTop = '12px'; LTD_TABS.forEach(({ label, id }, i) => { const btn = mkEl('button', { cls: 'ivy-tab' + (i === 0 ? ' is-active' : ''), text: label, }); btn.addEventListener('click', () => { tabBar.querySelectorAll('.ivy-tab').forEach(b => b.classList.remove('is-active')); btn.classList.add('is-active'); tabPanels.forEach(p => { if (p.id === id) showPanel(p); else hidePanel(p); }); }); tabBar.appendChild(btn); }); headerRow.after(tabBar); } } /* ================================================================ 2. BRAND BEST SELLERS Existing .s_bestseller_header has .bs-tab buttons and the Odoo dynamic product sliders are sibling sections immediately after it. Scope everything to those siblings so duplicate IDs elsewhere on the page do not steal the tab/navigation behavior. ================================================================ */ function setupBrandBestSellers() { const header = document.querySelector('.s_bestseller_header'); if (!header) return; const bsTabs = Array.from(header.querySelectorAll('.bs-tab')); if (!bsTabs.length) return; const tabContainer = bsTabs[0].parentElement; tabContainer.classList.add('ivy-tabs'); const panels = collectBestsellerPanels(header); function showPanel(panel) { panel.classList.remove('ivy-bs-hidden'); panel.classList.add('ivy-bs-visible', 'ivy-active'); panel.setAttribute('aria-hidden', 'false'); panel.style.setProperty('display', 'block', 'important'); panel.style.setProperty('visibility', 'visible', 'important'); panel.style.setProperty('opacity', '1', 'important'); panel.style.setProperty('height', 'auto', 'important'); panel.style.setProperty('overflow', 'visible', 'important'); panel.style.setProperty('padding-top', '0px', 'important'); panel.style.setProperty('padding-bottom', '48px', 'important'); panel.style.setProperty('margin-top', '0px', 'important'); panel.style.setProperty('margin-bottom', '0px', 'important'); } function hidePanel(panel) { if (!panel) return; panel.classList.remove('ivy-bs-visible', 'ivy-active'); panel.classList.add('ivy-bs-hidden'); panel.setAttribute('aria-hidden', 'true'); panel.style.setProperty('display', 'none', 'important'); panel.style.setProperty('visibility', 'hidden', 'important'); panel.style.setProperty('opacity', '0', 'important'); panel.style.setProperty('height', '0px', 'important'); panel.style.setProperty('overflow', 'hidden', 'important'); panel.style.setProperty('padding-top', '0px', 'important'); panel.style.setProperty('padding-bottom', '0px', 'important'); panel.style.setProperty('margin-top', '0px', 'important'); panel.style.setProperty('margin-bottom', '0px', 'important'); } function getActiveTarget() { const activeTab = bsTabs.find(tab => tab.classList.contains('is-active')) || bsTabs[0]; return activeTab ? activeTab.getAttribute('data-target') : null; } function getActivePanel() { const activeTarget = getActiveTarget(); const match = panels.find(item => item.target === activeTarget); return match ? match.el : null; } function activate(target) { const hasPanel = panels.some(item => item.target === target); if (!hasPanel) return; bsTabs.forEach(tab => { const isActive = tab.getAttribute('data-target') === target; tab.classList.toggle('active', isActive); tab.classList.toggle('is-active', isActive); tab.setAttribute('aria-selected', isActive ? 'true' : 'false'); }); panels.forEach(item => { if (item.target === target) showPanel(item.el); else hidePanel(item.el); }); } function triggerSlider(panel, direction) { if (!panel) return; const selector = direction === 'next' ? '.carousel-control-next, [data-bs-slide="next"], [data-slide="next"]' : '.carousel-control-prev, [data-bs-slide="prev"], [data-slide="prev"]'; const nativeControl = panel.querySelector(selector); if (nativeControl) { nativeControl.click(); return; } const owl = panel.querySelector('.owl-carousel'); if (owl && window.jQuery && window.jQuery.fn && window.jQuery.fn.owlCarousel) { window.jQuery(owl).trigger(direction + '.owl.carousel'); return; } const scrollTarget = panel.querySelector('.owl-stage-outer, .carousel-inner, .dynamic_snippet_template'); if (scrollTarget) { const delta = Math.max(240, scrollTarget.clientWidth * 0.8); scrollTarget.scrollBy({ left: direction === 'next' ? delta : -delta, behavior: 'smooth', }); } } bsTabs.forEach(tab => { tab.classList.add('ivy-tab'); tab.setAttribute('role', 'tab'); const target = tab.getAttribute('data-target'); if (panels.length && !panels.some(item => item.target === target)) { tab.hidden = true; return; } tab.addEventListener('click', function (ev) { ev.preventDefault(); activate(target); }); }); const navButtons = [ { id: 'bs-nav-prev', direction: 'prev' }, { id: 'bs-nav-next', direction: 'next' }, ]; navButtons.forEach(({ id, direction }) => { const btn = document.getElementById(id); if (!btn) return; btn.classList.add('ivy-nav-btn'); btn.setAttribute('type', 'button'); btn.addEventListener('click', function (ev) { ev.preventDefault(); triggerSlider(getActivePanel(), direction); }); }); const initialTab = bsTabs.find(tab => !tab.hidden && (tab.classList.contains('active') || tab.classList.contains('is-active')) ) || bsTabs.find(tab => !tab.hidden); if (initialTab) activate(initialTab.getAttribute('data-target')); header.dataset.ivyBbs = '1'; } function collectBestsellerPanels(header) { const panels = []; const seenTargets = new Set(); const tabTargets = new Set(Array.from(header.querySelectorAll('.bs-tab')) .map(tab => tab.getAttribute('data-target')) .filter(Boolean)); let node = header ? header.nextElementSibling : null; let scanned = 0; while (node && scanned < 20) { scanned += 1; if (!node.matches('section.s_dynamic_snippet_prod_temp')) { if (panels.length && node.matches('section')) break; node = node.nextElementSibling; continue; } const target = node.getAttribute('data-ivy-bs-slider') || node.getAttribute('data-ivy-bs-tab') || node.id; if (target && tabTargets.has(target) && !seenTargets.has(target)) { node.classList.add('ivy-bs-slider'); node.setAttribute('data-ivy-bs-slider', target); panels.push({ target, el: node }); seenTargets.add(target); } else if (target && tabTargets.has(target)) { hideBestsellerPanel(node); } node = node.nextElementSibling; } return panels; } function showBestsellerPanel(panel) { if (!panel) return; panel.classList.remove('ivy-bs-hidden'); panel.classList.add('ivy-bs-visible', 'ivy-active'); panel.setAttribute('aria-hidden', 'false'); panel.style.setProperty('display', 'block', 'important'); panel.style.setProperty('visibility', 'visible', 'important'); panel.style.setProperty('opacity', '1', 'important'); panel.style.setProperty('height', 'auto', 'important'); panel.style.setProperty('overflow', 'visible', 'important'); panel.style.setProperty('padding-top', '0px', 'important'); panel.style.setProperty('padding-bottom', '48px', 'important'); panel.style.setProperty('margin-top', '0px', 'important'); panel.style.setProperty('margin-bottom', '0px', 'important'); } function hideBestsellerPanel(panel) { if (!panel) return; panel.classList.remove('ivy-bs-visible', 'ivy-active'); panel.classList.add('ivy-bs-hidden'); panel.setAttribute('aria-hidden', 'true'); panel.style.setProperty('display', 'none', 'important'); panel.style.setProperty('visibility', 'hidden', 'important'); panel.style.setProperty('opacity', '0', 'important'); panel.style.setProperty('height', '0px', 'important'); panel.style.setProperty('overflow', 'hidden', 'important'); panel.style.setProperty('padding-top', '0px', 'important'); panel.style.setProperty('padding-bottom', '0px', 'important'); panel.style.setProperty('margin-top', '0px', 'important'); panel.style.setProperty('margin-bottom', '0px', 'important'); } function forceBestsellerActivate(target) { const header = document.querySelector('.s_bestseller_header'); if (!header || !target) return; const bsTabs = Array.from(header.querySelectorAll('.bs-tab')); const panels = collectBestsellerPanels(header); if (!panels.some(item => item.target === target)) return; bsTabs.forEach(tab => { const isActive = tab.getAttribute('data-target') === target; tab.classList.add('ivy-tab'); tab.classList.toggle('active', isActive); tab.classList.toggle('is-active', isActive); tab.setAttribute('aria-selected', isActive ? 'true' : 'false'); }); panels.forEach(item => { if (item.target === target) showBestsellerPanel(item.el); else hideBestsellerPanel(item.el); }); } function installBestsellerDelegates() { if (document.documentElement.dataset.ivyBbsDelegates === '1') return; document.documentElement.dataset.ivyBbsDelegates = '1'; document.addEventListener('click', function (ev) { const tab = ev.target.closest && ev.target.closest('.bs-tab'); const header = tab && tab.closest('.s_bestseller_header'); if (!tab || !header) return; ev.preventDefault(); ev.stopPropagation(); forceBestsellerActivate(tab.getAttribute('data-target')); }, true); } /* ================================================================ 3. WATCH & SHOP Keep Odoo video carousel and Odoo product slider as editable widgets. The injected code only links their navigation and trims product imagery. ================================================================ */ function setupWatchAndShop() { const sec = document.querySelector('.videos_carousel_snippet'); if (!sec) return; const productSec = findWatchProductSection(sec); resetWatchSyntheticState(sec, productSec); sec.classList.add('ivy-watch-linked'); sec.dataset.ivyWatchReady = '1'; if (productSec) { productSec.classList.add('ivy-watch-product-linked'); productSec.classList.remove('ivy-watch-product-source-hidden'); productSec.removeAttribute('aria-hidden'); ['display', 'visibility', 'height', 'max-height', 'overflow', 'padding-top', 'padding-bottom', 'margin-top', 'margin-bottom', 'opacity'].forEach(prop => { productSec.style.removeProperty(prop); }); hideWatchProductImages(productSec); } else { watchRetry(sec); } ensureWatchControls(sec); alignWatchOwlCarousels(sec, productSec); repeatWatchAlignment(sec); syncWatchButtonState(sec); } function resetWatchSyntheticState(sec, productSec) { sec.classList.remove('ivy-watch-ready', 'ivy-watch-shop-ready'); sec.querySelectorAll('.ivy-watch-shell, .ivy-watch-track, .ivy-watch-card, .ivy-watch-shop-paired, .ivy-watch-shop-nav').forEach(el => el.remove()); sec.querySelectorAll('.ivy-watch-controls').forEach((el, index) => { if (index > 0) el.remove(); }); if (productSec) { productSec.classList.remove('ivy-watch-product-source-hidden'); productSec.querySelectorAll('.ivy-watch-thumb-hidden, .ivy-watch-real-product-card').forEach(el => { el.classList.remove('ivy-watch-thumb-hidden', 'ivy-watch-real-product-card'); }); } } function ensureWatchControls(sec) { const header = Array.from(sec.children).find(el => el.classList && el.classList.contains('pb16')) || sec; let controls = header.querySelector('.ivy-watch-controls'); if (!controls) { controls = mkEl('div', { cls: 'ivy-watch-controls' }); const prev = mkEl('button', { cls: 'ivy-nav-btn ivy-watch-prev', html: chevronSvg('left') }); const next = mkEl('button', { cls: 'ivy-nav-btn ivy-watch-next', html: chevronSvg('right') }); prev.type = 'button'; next.type = 'button'; prev.setAttribute('aria-label', 'Previous'); next.setAttribute('aria-label', 'Next'); controls.appendChild(prev); controls.appendChild(next); header.appendChild(controls); } return controls; } function findWatchProductSection(sec) { let node = sec.nextElementSibling; while (node) { if (node.classList && node.classList.contains('s_bestseller_header')) return null; if ( node.matches && node.matches('section.s_dynamic_snippet_prod_temp') && ( node.classList.contains('ivy-video-product-section') || node.classList.contains('ivy-clean-video-product') || node.id === 'video-prod-slider' || node.querySelector('.dynamic_snippet_template, .owl-carousel, .carousel-inner') ) ) { return node; } node = node.nextElementSibling; } return null; } function installWatchDelegates() { if (document.documentElement.dataset.ivyWatchDelegates === '1') return; document.documentElement.dataset.ivyWatchDelegates = '1'; document.addEventListener('click', function (ev) { const btn = ev.target.closest && ev.target.closest('.ivy-watch-prev, .ivy-watch-next, .videos_carousel_snippet.ivy-watch-linked .owl-prev, .videos_carousel_snippet.ivy-watch-linked .owl-next, .ivy-watch-product-linked .owl-prev, .ivy-watch-product-linked .owl-next, .videos_carousel_snippet.ivy-watch-linked .carousel-control-prev, .videos_carousel_snippet.ivy-watch-linked .carousel-control-next, .ivy-watch-product-linked .carousel-control-prev, .ivy-watch-product-linked .carousel-control-next'); if (!btn) return; const sec = btn.closest('.videos_carousel_snippet') || findWatchVideoSectionFromProduct(btn.closest('.ivy-watch-product-linked')); if (!sec) return; ev.preventDefault(); ev.stopPropagation(); const direction = ( btn.classList.contains('ivy-watch-next') || btn.classList.contains('owl-next') || btn.classList.contains('carousel-control-next') ) ? 'next' : 'prev'; moveWatchPair(sec, direction); }, true); } function findWatchVideoSectionFromProduct(productSec) { if (!productSec) return null; let node = productSec.previousElementSibling; while (node) { if (node.classList && node.classList.contains('videos_carousel_snippet')) return node; node = node.previousElementSibling; } return document.querySelector('.videos_carousel_snippet.ivy-watch-linked'); } function moveWatchPair(sec, direction) { const productSec = findWatchProductSection(sec); const current = Number(sec.dataset.ivyWatchIndex || 0); const max = getWatchMaxIndex(sec, productSec); const nextIndex = Math.max(0, Math.min(current + (direction === 'next' ? 1 : -1), max)); sec.dataset.ivyWatchIndex = String(nextIndex); const movedVideo = goToOdooCarousel(sec.querySelector('#slide_video') || sec, nextIndex); const movedProduct = productSec ? goToOdooCarousel(productSec, nextIndex) : false; if (!movedVideo && !movedProduct) { fallbackWatchScroll(sec, productSec, direction); } setTimeout(() => syncWatchButtonState(sec), 80); } function alignWatchOwlCarousels(sec, productSec) { const videoCarousel = sec.querySelector('#slide_video.owl-carousel, #slide_video .owl-carousel') || sec.querySelector('#slide_video'); const productCarousel = productSec && (productSec.querySelector('.owl-carousel') || productSec); if (productSec) hideWatchProductImages(productSec); [videoCarousel, productCarousel].forEach(carousel => configureWatchOwl(carousel)); sec.dataset.ivyWatchIndex = sec.dataset.ivyWatchIndex || '0'; setTimeout(() => { goToOdooCarousel(videoCarousel, Number(sec.dataset.ivyWatchIndex || 0), true); goToOdooCarousel(productCarousel, Number(sec.dataset.ivyWatchIndex || 0), true); syncWatchButtonState(sec); }, 80); } function repeatWatchAlignment(sec) { if (sec.dataset.ivyWatchAligning === '1') return; sec.dataset.ivyWatchAligning = '1'; let count = 0; const timer = setInterval(() => { count += 1; const productSec = findWatchProductSection(sec); if (productSec) hideWatchProductImages(productSec); alignWatchOwlCarousels(sec, productSec); if (count >= 12) { clearInterval(timer); delete sec.dataset.ivyWatchAligning; } }, 350); } function configureWatchOwl(carousel) { if (!carousel || !window.jQuery) return false; const $carousel = window.jQuery(carousel); const owl = $carousel.data('owl.carousel') || $carousel.data('owlCarousel'); if (!owl) return false; try { owl.options.items = 5; owl.options.margin = 12; owl.options.loop = false; owl.options.nav = false; owl.options.dots = false; owl.options.autoplay = false; owl.options.responsive = { 0: { items: 2, margin: 10 }, 576: { items: 3, margin: 12 }, 992: { items: 5, margin: 12 }, }; $carousel.trigger('refresh.owl.carousel'); return true; } catch (err) { return false; } } function hideWatchProductImages(productSec) { if (!productSec) return; productSec.querySelectorAll('img, picture, source').forEach(el => { el.classList.add('ivy-watch-product-image-hidden'); const wrapper = el.closest('.oe_product_image, .o_carousel_product_img, .o_wsale_product_information_img, .product_img, .te_product_image, .img_block, .prod_img, .product_image, .o_wsale_product_image, .te_product_img, .product_image_container, .card-img-top, a[href*="/shop/"]'); if (wrapper && !wrapper.matches('button, .btn')) { wrapper.classList.add('ivy-watch-product-image-hidden'); } }); } function goToOdooCarousel(root, index, immediate) { if (!root) return false; const carousel = root.matches && root.matches('.owl-carousel') ? root : root.querySelector('.owl-carousel'); if (carousel && window.jQuery) { const $carousel = window.jQuery(carousel); const owlReady = Boolean($carousel.data('owl.carousel') || $carousel.data('owlCarousel')); if (owlReady) { $carousel.trigger('to.owl.carousel', [index, immediate ? 0 : 260, true]); return true; } } const carouselEl = root.querySelector('.carousel'); if (carouselEl && window.bootstrap && window.bootstrap.Carousel) { const instance = window.bootstrap.Carousel.getOrCreateInstance(carouselEl, { interval: false }); instance.to(index); return true; } return false; } function getWatchMaxIndex(sec, productSec) { const videoCount = countWatchItems(sec.querySelector('#slide_video') || sec); const productCount = productSec ? countWatchItems(productSec) : videoCount; const total = Math.max(videoCount, productCount); return Math.max(0, total - getWatchPerView()); } function countWatchItems(root) { if (!root) return 0; const carousel = root.matches && root.matches('.owl-carousel') ? root : root.querySelector('.owl-carousel'); if (carousel) { const items = carousel.querySelectorAll('.owl-item:not(.cloned)'); if (items.length) return items.length; } const carouselItems = root.querySelectorAll('.carousel-item'); if (carouselItems.length) return carouselItems.length; return root.querySelectorAll('.item, .video_play_banner, .card, .oe_product_cart, .o_carousel_product_card').length; } function triggerOdooCarousel(root, direction) { if (!root) return false; const carousel = root.matches && root.matches('.owl-carousel') ? root : root.querySelector('.owl-carousel'); if (carousel && window.jQuery) { const $carousel = window.jQuery(carousel); const owlReady = Boolean($carousel.data('owl.carousel') || $carousel.data('owlCarousel')); if (owlReady) { $carousel.trigger(direction + '.owl.carousel'); return true; } } const owlButton = root.querySelector(direction === 'next' ? '.owl-next' : '.owl-prev'); if (owlButton) { owlButton.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); return true; } const bsButton = root.querySelector(direction === 'next' ? '.carousel-control-next, [data-bs-slide="next"], [data-slide="next"]' : '.carousel-control-prev, [data-bs-slide="prev"], [data-slide="prev"]'); if (bsButton) { bsButton.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); return true; } const scrollRoot = root.querySelector('.owl-stage-outer, .carousel-inner, .dynamic_snippet_template') || root; if (scrollRoot && typeof scrollRoot.scrollBy === 'function' && scrollRoot.scrollWidth > scrollRoot.clientWidth) { const amount = Math.max(220, scrollRoot.clientWidth / 5); scrollRoot.scrollBy({ left: direction === 'next' ? amount : -amount, behavior: 'smooth' }); return true; } return false; } function fallbackWatchScroll(sec, productSec, direction) { [sec && sec.querySelector('#slide_video'), productSec].forEach(root => { if (!root || typeof root.scrollBy !== 'function') return; root.scrollBy({ left: (direction === 'next' ? 1 : -1) * Math.max(220, root.clientWidth / 5), behavior: 'smooth' }); }); } function syncWatchButtonState(sec) { const controls = sec.querySelector('.ivy-watch-controls'); if (!controls) return; const productSec = findWatchProductSection(sec); const index = Number(sec.dataset.ivyWatchIndex || 0); const max = getWatchMaxIndex(sec, productSec); const prev = controls.querySelector('.ivy-watch-prev'); const next = controls.querySelector('.ivy-watch-next'); if (prev) prev.toggleAttribute('disabled', index <= 0); if (next) next.toggleAttribute('disabled', index >= max); } function getWatchPerView() { if (window.matchMedia('(max-width: 575px)').matches) return 2; if (window.matchMedia('(max-width: 991px)').matches) return 3; return 5; } function watchRetry(sec) { const attempts = Number(sec.dataset.ivyWatchAttempts || 0); if (attempts >= 30 || sec.dataset.ivyWatchRetrying === '1') return; sec.dataset.ivyWatchAttempts = String(attempts + 1); sec.dataset.ivyWatchRetrying = '1'; const observer = new MutationObserver(() => { const productSec = findWatchProductSection(sec); if (productSec) { observer.disconnect(); delete sec.dataset.ivyWatchRetrying; setupWatchAndShop(); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); delete sec.dataset.ivyWatchRetrying; setupWatchAndShop(); }, 500); } /* ================================================================ 4. ANNOUNCEMENTS Find the s_three_columns after the "Announcements" s_title. Add .ivy-ann-styled so CSS re-styles the existing Odoo cards. ================================================================ */ function setupAnnouncements() { const wrap = document.getElementById('wrap'); if (!wrap) return; const sections = Array.from(wrap.children); const titleIdx = sections.findIndex(el => el.classList.contains('s_title') && el.textContent.includes('Announcements') ); if (titleIdx < 0) return; const annSec = sections[titleIdx + 1]; if (!annSec || !annSec.classList.contains('s_three_columns')) return; if (annSec.dataset.ivyAnn) return; annSec.dataset.ivyAnn = '1'; annSec.classList.add('ivy-ann-styled'); } function setupNewsSections() { const wrap = document.getElementById('wrap'); if (!wrap) return; const sections = Array.from(wrap.children); sections.forEach((section, index) => { if (!section.classList.contains('s_title')) return; const title = section.textContent.replace(/\s+/g, ' ').trim().toLowerCase(); const next = sections[index + 1]; if (!next || !next.classList.contains('news_carousel_snippet')) return; if (title.includes('announcements')) { renderAnnouncementNews(next); } else if (title.includes('ivykiss news')) { renderIvykissNews(next); } }); observeNewsDataChanges(wrap); } function parseNewsData(section) { const raw = section.getAttribute('data-blog-post-news') || '[]'; try { const data = JSON.parse(raw.replace(/"|"/g, '"')); return Array.isArray(data) ? data : []; } catch (err) { return []; } } function clearGeneratedNews(section) { section.querySelectorAll('.ivy-ann-news, .ivy-news-board, .ivy-news-generated').forEach(el => el.remove()); section.querySelectorAll(':scope > .container').forEach(el => { if (el.querySelector('.ivy-news-list-header, .ivy-news-list, .ivy-news-grid')) el.remove(); }); } function newsHref(item) { return item.id ? `/blog?post=${encodeURIComponent(item.id)}` : '/blog'; } function newsSignature(section) { return section.getAttribute('data-blog-post-news') || '[]'; } function renderAnnouncementNews(section) { const signature = newsSignature(section); if (section.dataset.ivyAnnNews === signature && section.querySelector('.ivy-ann-news')) return; const posts = parseNewsData(section).slice(0, 3); if (!posts.length) return; clearGeneratedNews(section); section.dataset.ivyAnnNews = signature; section.classList.add('ivy-ann-news-section'); const shell = mkEl('div', { cls: 'ivy-ann-news' }); const list = mkEl('div', { cls: 'ivy-ann-news-list' }); posts.forEach((post, idx) => { const item = mkEl('article', { cls: 'ivy-ann-news-item' }); const title = post.display_name || post.name || 'Shipping Delay Notice: Severe Winter Storm'; item.innerHTML = ` ${escapeNewsHtml(title)}

Due to extreme weather conditions, your shipment may experience delays as we prioritize safety and delivery reliability.

2026.04.01
`; list.appendChild(item); }); shell.appendChild(list); section.appendChild(shell); } function renderIvykissNews(section) { const signature = newsSignature(section); if (section.dataset.ivyNewsBoard === signature && section.querySelector('.ivy-news-board')) return; const posts = parseNewsData(section).slice(0, 8); if (!posts.length) return; clearGeneratedNews(section); section.dataset.ivyNewsBoard = signature; section.classList.add('ivy-news-board-section'); const groups = [ { title: 'Business Updates', items: posts.slice(0, 4), chip: 'NEWS' }, { title: 'Marketing News', items: posts.slice(4, 8), chip: 'REPORT' }, ]; if (!groups[1].items.length) groups[1].items = posts.slice(0, 4); const shell = mkEl('div', { cls: 'ivy-news-board' }); const grid = mkEl('div', { cls: 'ivy-news-board-grid' }); groups.forEach(group => { const panel = mkEl('div', { cls: 'ivy-news-board-panel' }); const list = mkEl('div', { cls: 'ivy-news-board-list' }); group.items.forEach((post, idx) => { const item = mkEl('div', { cls: 'ivy-news-board-item' }); const title = post.display_name || post.name || 'News'; item.innerHTML = `
${idx % 2 ? group.chip : 'NEWS'}
${escapeNewsHtml(title)} Latest IVYKISS content from the news carousel.
${group.chip === 'REPORT' ? 'Download' : 'Read'} `; list.appendChild(item); }); panel.innerHTML = `
${group.title} View All
`; panel.appendChild(list); grid.appendChild(panel); }); shell.appendChild(grid); section.appendChild(shell); } function escapeNewsHtml(value) { return String(value == null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function observeNewsDataChanges(wrap) { if (wrap.dataset.ivyNewsObserver === '1') return; wrap.dataset.ivyNewsObserver = '1'; const observer = new MutationObserver(mutations => { const shouldRefresh = mutations.some(mutation => mutation.type === 'attributes' && mutation.attributeName === 'data-blog-post-news' && mutation.target.classList && mutation.target.classList.contains('news_carousel_snippet') ); if (shouldRefresh) setupNewsSections(); }); observer.observe(wrap, { subtree: true, attributes: true, attributeFilter: ['data-blog-post-news'], }); } /* ================================================================ BOOT ================================================================ */ function boot() { setupLimitedTimeDeals(); installBestsellerDelegates(); installWatchDelegates(); setupBrandBestSellers(); setupWatchAndShop(); setupAnnouncements(); setupNewsSections(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } /* Re-run on full load in case Odoo dynamic snippets rendered late */ window.addEventListener('load', () => { if (!document.querySelector('[data-ivy-ltd]')) setupLimitedTimeDeals(); installBestsellerDelegates(); installWatchDelegates(); setupBrandBestSellers(); setupWatchAndShop(); if (!document.querySelector('[data-ivy-ann]')) setupAnnouncements(); setupNewsSections(); }); })();