관리-도구
편집 파일: luxury-theme.js
/** * SLNS SAREES — COUTURE EDITION JS * Preloader curtain · custom cursor · hero letter-stagger · * clip-path card reveals · filters with counts · parallax bands · * footer wordmark · marquee · mobile nav · stock cleanup. */ (function () { 'use strict'; const $ = s => document.querySelector(s); const $$ = s => [...document.querySelectorAll(s)]; const on = (el, e, f, o) => el && el.addEventListener(e, f, o); const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches; /* ═ 1 · PRELOADER — video splash, lifts when video ends or window loads ═ */ (function loader() { if (reduced || sessionStorage.getItem('cxSeen')) return; const l = document.createElement('div'); l.className = 'cx-loader'; const isPublic = window.location.pathname.includes('/public') || document.querySelector('script[src*="/public/assets/"]') !== null; const videoSrc = isPublic ? '/public/assets/front-end/hero1.mp4' : '/assets/front-end/hero1.mp4'; l.innerHTML = ` <video class="cx-loader__video" autoplay muted playsinline style="width: 100%; height: 100%; object-fit: cover; position: absolute; inset: 0;"> <source src="${videoSrc}" type="video/mp4"> </video> <div class="cx-loader__line"></div> `; document.documentElement.appendChild(l); const lift = () => { if (l.classList.contains('done')) return; l.classList.add('done'); sessionStorage.setItem('cxSeen', '1'); setTimeout(() => l.remove(), 1400); }; const video = l.querySelector('video'); if (video) { video.addEventListener('ended', lift); let failsafeTimer = null; const setupFailsafe = () => { if (failsafeTimer) return; const durationSec = video.duration || 10; const delayMs = (durationSec * 1000) + 3000; failsafeTimer = setTimeout(lift, delayMs); }; video.addEventListener('loadedmetadata', setupFailsafe); video.addEventListener('canplaythrough', setupFailsafe); // Attempt explicit play trigger video.play().then(() => { if (video.duration) { setupFailsafe(); } else { failsafeTimer = setTimeout(lift, 25000); } }).catch(() => { // If autoplay is blocked by browser policies, lift quickly so user isn't stuck setTimeout(lift, 2500); }); } else { window.addEventListener('load', () => setTimeout(lift, 600)); setTimeout(lift, 3200); // failsafe } })(); /* ═ 2 · CUSTOM CURSOR ═ */ function cursor() { if (reduced || matchMedia('(pointer: coarse)').matches) return; const dot = document.createElement('div'); dot.className = 'cx-cursor'; const ring = document.createElement('div'); ring.className = 'cx-cursor-ring'; document.body.append(dot, ring); let mx = -50, my = -50, rx = -50, ry = -50; on(document, 'mousemove', e => { mx = e.clientX; my = e.clientY; }, { passive: true }); (function loop() { rx += (mx - rx) * 0.16; ry += (my - ry) * 0.16; dot.style.transform = `translate(${mx}px,${my}px) translate(-50%,-50%)`; ring.style.transform = `translate(${rx}px,${ry}px) translate(-50%,-50%)`; requestAnimationFrame(loop); })(); on(document, 'mouseover', e => { if (e.target.closest('a,button,.cxp,.luxury-product-card,input,select')) ring.classList.add('is-link'); }); on(document, 'mouseout', e => { if (e.target.closest('a,button,.cxp,.luxury-product-card,input,select')) ring.classList.remove('is-link'); }); } /* ═ 3 · HERO LETTER-STAGGER — splits .cxh__title into masks ═ */ function splitHero() { const titles = $$('.cxh__title'); titles.forEach(t => { if (!t || t.dataset.split || reduced) return; t.dataset.split = '1'; let letterIndex = 0; const walk = node => { [...node.childNodes].forEach(n => { if (n.nodeType === 3) { const frag = document.createDocumentFragment(); n.textContent.split(/(\s+)/).forEach(word => { if (!word) return; if (/^\s+$/.test(word)) { frag.appendChild(document.createTextNode(' ')); return; } const w = document.createElement('span'); w.className = 'w'; [...word].forEach(chr => { const c = document.createElement('span'); c.className = 'ch'; c.textContent = chr; c.style.animationDelay = `${0.55 + letterIndex * 0.028}s`; letterIndex++; w.appendChild(c); }); frag.appendChild(w); }); n.replaceWith(frag); } else if (n.nodeType === 1 && !n.classList.contains('w')) { walk(n); } }); }; walk(t); }); } /* ═ 4 · STICKY HEADER ═ */ function header() { const s = $('.luxury-header .navbar-sticky'); const t = $('.luxury-header .topbar'); if (!s) return; let prev = 0, tick = false; on(window, 'scroll', () => { if (tick) return; tick = true; requestAnimationFrame(() => { const y = scrollY; s.classList.toggle('scrolled', y > 64); if (t) { t.style.transition = 'transform 0.45s cubic-bezier(0.19,1,0.22,1)'; t.style.transform = (y > prev && y > 120) ? 'translateY(-100%)' : ''; } prev = y; tick = false; }); }, { passive: true }); } /* ═ 5 · FILTERS with live counts ═ */ function filters() { $$('.cxf').forEach(bar => { const grid = document.querySelector(bar.dataset.target || '.cxg'); if (!grid) return; // populate counts bar.querySelectorAll('.cxf__tab').forEach(tab => { const f = tab.dataset.filter || 'all'; const n = f === 'all' ? grid.children.length : [...grid.children].filter(c => (c.dataset.cat || '') === f).length; if (!tab.querySelector('sup')) { const s = document.createElement('sup'); s.textContent = n; tab.appendChild(s); } }); bar.addEventListener('click', e => { const tab = e.target.closest('.cxf__tab'); if (!tab) return; e.preventDefault(); bar.querySelectorAll('.cxf__tab').forEach(x => x.classList.remove('active')); tab.classList.add('active'); const want = tab.dataset.filter || 'all'; [...grid.children].forEach(card => { card.classList.toggle('cx-out', !(want === 'all' || (card.dataset.cat || '') === want)); }); }); }); } /* ═ 6 · PHOTO BANDS — parallax from site's own banner ═ */ function photoBands() { $$('.cxz--photo').forEach(band => { if (band.style.backgroundImage) return; const src = document.querySelector(band.dataset.photoFrom || '.cxh__media img')?.src; if (src) band.style.backgroundImage = `url('${src}')`; else band.classList.add('cxz--lacquer'); }); } /* ═ 7 · REVEALS + card clip reveal ═ */ function reveals() { const els = $$('.cxr, .cxp:not(.in)'); if (!('IntersectionObserver' in window)) { els.forEach(x => { x.classList.add('on', 'in'); }); return; } const io = new IntersectionObserver(es => es.forEach(e => { if (!e.isIntersecting) return; e.target.classList.add('on', 'in'); io.unobserve(e.target); }), { threshold: 0.08, rootMargin: '0px 0px -34px 0px' }); els.forEach(x => io.observe(x)); } /* ═ 8 · GHOST WATERMARK DRIFT ═ */ function ghost() { const gs = $$('.cxz__ghost'); if (!gs.length || reduced) return; let tick = false; on(window, 'scroll', () => { if (tick) return; tick = true; requestAnimationFrame(() => { gs.forEach(g => { const r = g.closest('.cxz')?.getBoundingClientRect(); if (!r || r.bottom < 0 || r.top > innerHeight) return; const p = (innerHeight - r.top) / (innerHeight + r.height); g.style.transform = `translate(-50%, calc(-50% + ${(p - 0.5) * 70}px))`; }); tick = false; }); }, { passive: true }); } /* ═ 9 · MARQUEE (auto-inject after hero) ═ */ function marquee() { if ($('.cxmq') || !$('.luxury-home')) return; const el = document.createElement('div'); el.className = 'cxmq'; const w = 'Pure Silk · Handloom Heritage · Since 1982 · Salem, Tamil Nadu · Woven With Love'; el.innerHTML = `<div class="cxmq__t"><span>${w}</span><span>${w}</span></div>`; $('.cxh')?.insertAdjacentElement('afterend', el); } /* ═ 10 · FOOTER GIANT WORDMARK ═ */ function wordmark() { const f = $('.luxury-footer'); if (!f || f.querySelector('.cx-wordmark')) return; const w = document.createElement('div'); w.className = 'cx-wordmark'; w.innerHTML = 'SLNS <em>Silks</em>'; f.insertBefore(w, f.firstChild); } /* ═ 11 · MOBILE NAV ═ */ function mnav() { if ($('.cxnav') || innerWidth > 768) return; const cart = $('[href*="cart"]')?.href || '/cart'; const wish = $('[href*="wishlists"]')?.href || '/wishlists'; const n = document.createElement('nav'); n.className = 'cxnav'; n.innerHTML = `<div class="cxnav__r"> <a class="cxnav__i${location.pathname === '/' ? ' active' : ''}" href="/"><i class="tio-home-outlined"></i><span>Home</span></a> <a class="cxnav__i" href="/products"><i class="tio-shop"></i><span>Shop</span></a> <a class="cxnav__i cxnav__i--cart" href="${cart}"><i class="tio-shopping-cart-add"></i><span>Cart</span></a> <a class="cxnav__i" href="${wish}"><i class="tio-heart-outlined"></i><span>Saved</span></a> <a class="cxnav__i" href="/customer/profile"><i class="tio-user-outlined"></i><span>Me</span></a> </div>`; document.body.appendChild(n); } /* ═ 12 · STAT COUNT-UP ═ */ function stats() { const els = $$('.cxh__stat strong'); if (!els.length || !('IntersectionObserver' in window)) return; const io = new IntersectionObserver(es => es.forEach(e => { if (!e.isIntersecting) return; io.unobserve(e.target); const raw = e.target.textContent.trim(); const n = parseInt(raw.replace(/[^\d]/g, ''), 10); const suf = raw.replace(/[\d,]/g, ''); if (!n) return; const t0 = performance.now(), dur = 1600; (function step(t) { const p = Math.min((t - t0) / dur, 1); e.target.textContent = Math.round(n * (1 - Math.pow(1 - p, 3))).toLocaleString('en-IN') + suf; if (p < 1) requestAnimationFrame(step); })(t0); }), { threshold: 0.4 }); els.forEach(x => io.observe(x)); } /* ═ 13 · STOCK-PARTIAL CLEANUP (live-site audit fixes) ═ */ function cleanup() { $$('.flash_deal_product_details').forEach(box => { [...box.childNodes].forEach(n => { if (n.nodeType === 3 && /quick\s*view|view\s*details/i.test(n.textContent)) n.remove(); }); box.querySelectorAll(':scope > a').forEach(a => { if (/^\s*(quick view|view details)\s*$/i.test(a.textContent)) a.style.display = 'none'; }); }); $$('a, button').forEach(el => { if (/grab this deal/i.test(el.textContent) && !el.dataset.cx) { el.dataset.cx = '1'; el.style.cssText += ';background:var(--c-gold)!important;border:none!important;border-radius:0!important;color:var(--c-black)!important;font-family:var(--f-b)!important;font-size:10px!important;font-weight:700!important;letter-spacing:0.24em!important;text-transform:uppercase!important;padding:15px 32px!important;'; const card = el.closest('.card, [class*="deal"]'); if (card) { card.style.cssText += ';background:var(--c-black)!important;border:1px solid rgba(176,141,87,0.3)!important;border-radius:0!important;'; card.querySelectorAll('h1,h2,h3,h4,h5,.product-title').forEach(t => t.style.color = 'var(--c-bone)'); const head = card.querySelector('.card-header, [class*="header"]'); if (head) head.style.cssText += ';background:transparent!important;border-bottom:1px solid rgba(176,141,87,0.3)!important;color:var(--c-gold)!important;font-family:var(--f-b)!important;font-size:9.5px!important;letter-spacing:0.36em!important;text-transform:uppercase!important;text-align:center!important;'; } } }); $$('h2, h3, .h4').forEach(h => { const t = h.textContent.trim(); if (t.length > 6 && t === t.toUpperCase() && /SAREE|SILK|COTTON|COLLECTION|KHADI|KALYANI|KALAMKARI/i.test(t) && !h.dataset.cxT) { h.dataset.cxT = '1'; h.style.cssText += ';font-family:var(--f-d)!important;font-weight:500!important;text-transform:capitalize!important;letter-spacing:-0.01em!important;font-size:clamp(1.7rem,3.2vw,2.6rem)!important;'; h.textContent = t.toLowerCase().replace(/(^|\s|-)\w/g, c => c.toUpperCase()); } }); } /* ═ 14 · TAG AJAX-LOADED CARDS ═ */ function tag() { $$('.luxury-product-card:not(.cxr), .flash_deal_product:not(.cxr)').forEach((c, i) => { c.classList.add('cxr'); if (i < 6) c.classList.add(`cxd${(i % 6) + 1}`); }); } /* ═ BOOT ═ */ function boot() { cursor(); header(); marquee(); wordmark(); mnav(); splitHero(); setTimeout(() => { filters(); photoBands(); tag(); reveals(); ghost(); stats(); cleanup(); }, 380); setTimeout(() => { tag(); reveals(); cleanup(); photoBands(); }, 1400); if ('MutationObserver' in window) { new MutationObserver(ms => { if (ms.some(m => [...m.addedNodes].some(n => n.nodeType === 1 && (n.matches?.('.luxury-product-card,.flash_deal_product,.cxp') || n.querySelector?.('.luxury-product-card,.cxp'))))) { tag(); reveals(); } }).observe(document.body, { childList: true, subtree: true }); } } document.readyState !== 'loading' ? boot() : document.addEventListener('DOMContentLoaded', boot); })(); /* ═══════════════════════════════════════════════════════════════ VIOLET COURT — JS revision (04-07 screenshots) ═══════════════════════════════════════════════════════════════ */ (function () { 'use strict'; const $$ = s => [...document.querySelectorAll(s)]; /* A · SOLD-OUT REORDERING — available sarees first in every grid. Works on the Edit grid (.cxg) and any owl/stock product row. */ function reorderSoldOut() { const isSold = el => /out of stock|sold out/i.test(el.textContent || ''); // The Edit grid + featured-deal grids $$('.cxg, .cx-product-row').forEach(grid => { const cards = [...grid.children].filter(c => c.matches('.cxp, .luxury-product-card, [class*="product"]')); if (cards.length < 2) return; const sold = [], avail = []; cards.forEach(c => { if (isSold(c)) { c.classList.add('cx-soldout'); sold.push(c); } else avail.push(c); }); // Re-append: available first, sold-out last [...avail, ...sold].forEach(c => grid.appendChild(c)); }); // Owl carousels (category-wise, new arrivals): reorder owl-items $$('.owl-stage').forEach(stage => { const items = [...stage.children].filter(i => i.classList.contains('owl-item') && !i.classList.contains('cloned')); if (items.length < 2) return; const sold = [], avail = []; items.forEach(i => { if (isSold(i)) { i.querySelector('.luxury-product-card, .cxp')?.classList.add('cx-soldout'); sold.push(i); } else avail.push(i); }); [...avail, ...sold].forEach(i => stage.appendChild(i)); }); } /* B · SPARSE CATEGORY ROWS → elegant spotlight invitation. Category-wise sections with only 1–2 products look empty (screen 4: lone "Arani Silk Sarees"). We flag the section and inject an invitation panel beside the product. */ function spotlightSparse() { $$('[class*="category-wise"], .category-wise-product, .cate-wise-product').forEach(sec => { if (sec.dataset.cxSparse) return; // Count real product cards in this section const cards = sec.querySelectorAll('.luxury-product-card, .cxp, [class*="single-product"]'); const container = sec.querySelector('.row, [class*="product-list"], .owl-stage'); if (!container) return; if (cards.length > 0 && cards.length <= 2) { sec.dataset.cxSparse = '1'; sec.classList.add('cx-sparse'); // Section title for context const titleEl = sec.querySelector('h2, .category-wise-title, h3'); const catName = titleEl ? titleEl.textContent.trim() : 'This Collection'; // Try to find the "view all" link for the button href const viewAll = sec.querySelector('a[href*="category"], a[href*="products"]'); const href = viewAll ? viewAll.href : '#'; // Build invitation panel const invite = document.createElement('div'); invite.className = 'cx-invite'; invite.innerHTML = ` <span class="cx-invite__k">Newly Curated</span> <div class="cx-invite__t">The <em>${catName}</em><br>collection awaits.</div> <p class="cx-invite__p">A considered edit, growing with every season. Discover the pieces we've woven with care — more arriving soon.</p> <a class="cx-invite__btn" href="${href}">Explore the Collection</a> `; container.appendChild(invite); } else { sec.dataset.cxSparse = '1'; // mark checked } }); } /* C · RE-COUNT filter tabs AFTER reorder (counts must be right) */ function recountFilters() { $$('.cxf').forEach(bar => { const grid = document.querySelector(bar.dataset.target || '.cxg'); if (!grid) return; bar.querySelectorAll('.cxf__tab').forEach(tab => { const f = tab.dataset.filter || 'all'; const n = f === 'all' ? grid.children.length : [...grid.children].filter(c => (c.dataset.cat || '') === f).length; let sup = tab.querySelector('sup'); if (!sup) { sup = document.createElement('sup'); tab.appendChild(sup); } sup.textContent = n; }); }); } function run() { reorderSoldOut(); spotlightSparse(); recountFilters(); } // Run after base boot renders everything if (document.readyState !== 'loading') setTimeout(run, 700); else document.addEventListener('DOMContentLoaded', () => setTimeout(run, 700)); setTimeout(run, 1700); window.addEventListener('load', () => setTimeout(run, 400)); })(); /* ═══════════════════════════════════════════════════════════════ SLNS MAISON HEADER — controller (overlay/solid, dropdowns, drawer) ═══════════════════════════════════════════════════════════════ */ (function () { 'use strict'; function initSlnsHeader() { const header = document.getElementById('slns-header'); if (!header) return; const hero = document.querySelector('.cxh, .velour-hero, .home-banner-slider'); /* overlay ↔ solid on scroll (only when a hero exists) */ if (hero) { const threshold = () => Math.max(120, hero.offsetHeight * 0.6); let ticking = false; const update = () => { header.classList.toggle('slns-solid', window.scrollY > threshold()); ticking = false; }; window.addEventListener('scroll', () => { if (!ticking) { requestAnimationFrame(update); ticking = true; } }, { passive: true }); update(); } else { header.classList.add('slns-nohero'); } /* categories dropdown — click to toggle, click-away to close */ const cat = header.querySelector('.slns-cat'); const catBtn = header.querySelector('.slns-cat__btn'); if (cat && catBtn) { catBtn.addEventListener('click', e => { e.stopPropagation(); cat.classList.toggle('open'); catBtn.setAttribute('aria-expanded', cat.classList.contains('open')); }); document.addEventListener('click', e => { if (!cat.contains(e.target)) cat.classList.remove('open'); }); /* also open on hover (desktop nicety) */ cat.addEventListener('mouseenter', () => { if (window.innerWidth > 991) cat.classList.add('open'); }); cat.addEventListener('mouseleave', () => { if (window.innerWidth > 991) cat.classList.remove('open'); }); } /* mobile burger → drawer */ const burger = document.getElementById('slns-burger'); const drawer = document.getElementById('slns-drawer'); if (burger && drawer) { burger.addEventListener('click', () => { burger.classList.toggle('open'); drawer.classList.toggle('open'); }); } /* mobile search toggle — reveal the (hidden) search inline */ const msearch = header.querySelector('.slns-tool--msearch'); const search = header.querySelector('.slns-search'); if (msearch && search) { msearch.addEventListener('click', () => { const shown = search.style.display === 'flex'; search.style.display = shown ? 'none' : 'flex'; search.style.position = 'absolute'; search.style.top = '100%'; search.style.left = '0'; search.style.right = '0'; search.style.maxWidth = 'none'; search.style.padding = '12px 18px'; search.style.background = 'rgba(42,18,48,.97)'; if (!shown) search.querySelector('input')?.focus(); }); } /* Product Inquiry Modal Controller */ const phoneTrigger = document.getElementById('slns-phone-trigger'); const contactModal = document.getElementById('slns-contact-modal'); const contactClose = document.getElementById('slns-contact-close'); const contactForm = document.getElementById('slns-contact-form'); const backdrop = contactModal?.querySelector('.slns-modal__backdrop'); if (phoneTrigger && contactModal) { phoneTrigger.addEventListener('click', (e) => { e.preventDefault(); contactModal.classList.add('open'); document.body.style.overflow = 'hidden'; }); const closeModal = () => { contactModal.classList.remove('open'); document.body.style.overflow = ''; }; contactClose?.addEventListener('click', closeModal); backdrop?.addEventListener('click', closeModal); if (contactForm) { contactForm.addEventListener('submit', (e) => { e.preventDefault(); const name = document.getElementById('slns-inq-name').value; const phone = document.getElementById('slns-inq-phone').value; const model = document.getElementById('slns-inq-model').value; const type = document.getElementById('slns-inq-type').value; const email = contactForm.getAttribute('data-email') || 'srilakshminarayanasilk@gmail.com'; const subject = encodeURIComponent(`Product Inquiry - ${model}`); const body = encodeURIComponent( `Hello Sri Lakshmi Narayana Silks,\n\n` + `I would like to inquire about the following saree:\n\n` + `- Customer Name: ${name}\n` + `- Contact Number: ${phone}\n` + `- Saree Model: ${model}\n` + `- Saree Type / Details: ${type}\n\n` + `Please get back to me with the details and pricing.\n\n` + `Regards,\n` + `${name}` ); window.location.href = `mailto:${email}?subject=${subject}&body=${body}`; }); } } } document.readyState !== 'loading' ? initSlnsHeader() : document.addEventListener('DOMContentLoaded', initSlnsHeader); window.addEventListener('load', initSlnsHeader); })(); /* ═══════════════════════════════════════════════════════════════ PILLAR PAGES — scroll-in reveal for listing cards (.product-with-bg) Works with AJAX pagination/filtering (MutationObserver re-tags). ═══════════════════════════════════════════════════════════════ */ (function () { 'use strict'; function revealShopCards() { const cards = document.querySelectorAll('.product-with-bg:not([data-cxr])'); if (!cards.length) return; if (!('IntersectionObserver' in window)) { cards.forEach(c => { c.dataset.cxr = '1'; c.classList.add('cxr-in'); }); return; } const io = new IntersectionObserver(es => es.forEach(e => { if (!e.isIntersecting) return; e.target.classList.add('cxr-in'); io.unobserve(e.target); }), { threshold: 0.06, rootMargin: '0px 0px -24px 0px' }); cards.forEach((c, i) => { c.dataset.cxr = '1'; c.style.transitionDelay = `${(i % 4) * 70}ms`; // subtle row stagger io.observe(c); }); } function boot() { revealShopCards(); // AJAX product loads (filters, pagination, sorting) re-render the grid if ('MutationObserver' in window) { new MutationObserver(ms => { if (ms.some(m => [...m.addedNodes].some(n => n.nodeType === 1 && (n.matches?.('.product-with-bg') || n.querySelector?.('.product-with-bg')) ))) setTimeout(revealShopCards, 80); }).observe(document.body, { childList: true, subtree: true }); } } document.readyState !== 'loading' ? boot() : document.addEventListener('DOMContentLoaded', boot); window.addEventListener('load', () => setTimeout(revealShopCards, 300)); })();