document.addEventListener('DOMContentLoaded',function(){ const toggle=document.querySelector('[data-mobile-toggle]'); const menu=document.querySelector('[data-mobile-menu]'); if(toggle&&menu){toggle.addEventListener('click',()=>{const open=menu.classList.toggle('is-open');toggle.setAttribute('aria-expanded',open?'true':'false')})} document.querySelectorAll('[data-qty]').forEach(w=>{const i=w.querySelector('input');w.querySelectorAll('[data-qty-action]').forEach(b=>b.addEventListener('click',()=>{let v=Math.max(1,parseInt(i.value||'1',10));i.value=b.dataset.qtyAction==='plus'?v+1:Math.max(1,v-1)}))}); const main=document.querySelector('[data-gallery-main]'); if(main){document.querySelectorAll('[data-gallery-thumb]').forEach(b=>b.addEventListener('click',()=>{main.src=b.dataset.src;document.querySelectorAll('[data-gallery-thumb]').forEach(x=>x.classList.remove('is-active'));b.classList.add('is-active')}))} // KOCARTS v3.1: automatically connect homepage category cards/menu labels // to real YouCan categories when the merchant has not selected them manually. const normalizeKoc = (v='') => String(v).trim().toLowerCase().replace(/[\u064B-\u065F\u0670]/g,'').replace(/\s+/g,' '); const autoCategoryLinks = [...document.querySelectorAll('[data-koc-category-auto]')]; if(autoCategoryLinks.length){ fetch('/api/categories') .then(r=>r.ok?r.json():Promise.reject(new Error('categories'))) .then(payload=>{ const categories = Array.isArray(payload) ? payload : (payload && Array.isArray(payload.data) ? payload.data : []); autoCategoryLinks.forEach((a,idx)=>{ const wanted = normalizeKoc(a.dataset.kocCategoryAuto || ''); let cat = categories.find(c=>normalizeKoc(c.name)===wanted || normalizeKoc(c.slug)===wanted); if(!cat && categories.length===3) cat=categories[idx]; if(cat){ a.href = cat.public_url || ('/collections/'+cat.slug); a.dataset.kocResolved='true'; } }); }) .catch(()=>{}); } // KOCARTS v4.2: products are managed in YouCan Admin, not in the theme editor. // The storefront grid syncs saved products automatically through YouCan's StoreFront API. document.querySelectorAll('[data-koc-auto-products]').forEach(autoProducts=>{ const limit = Math.max(1, Math.min(12, parseInt(autoProducts.dataset.limit || '6',10))); const currency = autoProducts.dataset.currency || 'درهم'; const categoryFilter = (autoProducts.dataset.category || '').trim(); const sortField = autoProducts.dataset.sort || 'created_at'; const hideSoldout = autoProducts.dataset.hideSoldout === 'true'; const grid = autoProducts.closest('[data-koc-products-grid]') || autoProducts.parentElement; const esc = (v='') => String(v).replace(/[&<>'"]/g,ch=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[ch])); const fmt = n => { const num = Number(n); return Number.isFinite(num) ? new Intl.NumberFormat('ar-MA',{maximumFractionDigits:2}).format(num)+' '+esc(currency) : ''; }; const normalize = (v='') => String(v).trim().toLowerCase().replace(/[\u064B-\u065F\u0670]/g,'').replace(/\s+/g,' '); const renderProducts = (products=[]) => { let shown = 0; products.forEach(product=>{ if(shown >= limit) return; if(hideSoldout && product.track_inventory && Number(product.inventory) <= 0) return; const url = product.public_url || (product.slug ? '/products/'+product.slug : '#'); const image = product.thumbnail || (product.images && product.images[0] && (product.images[0].url || (product.images[0].variations && product.images[0].variations.md))) || ''; const onSale = Number(product.compare_at_price) > Number(product.price); const card = document.createElement('article'); card.className='koc-product-card koc-product-card--synced'; card.innerHTML=` ${image?``:``}${onSale?'عرض':''}♡ KOCARTS${esc(product.name)}${fmt(product.price)}${onSale?`${fmt(product.compare_at_price)}`:''}عرض التفاصيل ← `; grid.appendChild(card); shown++; }); if(!shown && !grid.querySelector('.koc-product-card')){ const empty=document.createElement('div'); empty.className='koc-products-empty'; empty.innerHTML='لا توجد منتجات ظاهرة بعدأضف منتجًا من لوحة YouCan واجعله ظاهرًا في المتجر، وسيظهر هنا تلقائيًا.'; grid.appendChild(empty); } }; const fetchJson = url => fetch(url,{headers:{'Accept':'application/json'}}).then(r=>r.ok?r.json():Promise.reject(new Error('request'))); const fetchAll = () => fetchJson('/api/products?include=images&per_page='+limit+'&sort_field='+encodeURIComponent(sortField)).then(payload=>Array.isArray(payload&&payload.data)?payload.data:[]); if(categoryFilter){ fetchJson('/api/categories') .then(payload=>{ const categories=Array.isArray(payload)?payload:(Array.isArray(payload&&payload.data)?payload.data:[]); const wanted=normalize(categoryFilter); const category=categories.find(c=>normalize(c.name)===wanted || normalize(c.slug)===wanted); if(!category) return fetchAll(); return fetchJson('/api/categories/'+encodeURIComponent(category.id)+'/products?include=images&per_page='+limit).then(payload=>Array.isArray(payload&&payload.data)?payload.data:[]); }) .then(renderProducts) .catch(()=>fetchAll().then(renderProducts).catch(()=>renderProducts([]))); }else{ fetchAll().then(renderProducts).catch(()=>renderProducts([])); } }); });