import { addItem, formatPrice } from "/js/cart.js"; import { ensureEurRate, sellCompareFromItem, sellPriceFromItem, } from "/js/fx.js?v=fx1"; function moneyItem(product, variant) { const v = variant || {}; return { price: v.price ?? product?.price, compareAtPrice: v.compareAtPrice ?? product?.compareAtPrice, priceEur: v.priceEur ?? product?.priceEur, sourcePrice: v.sourcePrice ?? product?.sourcePrice, sourceCurrency: v.sourceCurrency ?? product?.sourceCurrency, }; } import { loveYourEyesPillHtml } from "/js/love-your-eyes.js?v=lyeOff"; export function initBuyButtons() { document.addEventListener("click", (event) => { const btn = event.target instanceof Element ? event.target.closest("[data-add-to-cart]") : null; if (!(btn instanceof HTMLElement)) return; if (btn.hasAttribute("disabled")) return; const productId = btn.getAttribute("data-product-id"); const slug = btn.getAttribute("data-slug"); const name = btn.getAttribute("data-name"); const price = Number(btn.getAttribute("data-price")); const image = btn.getAttribute("data-image"); const category = btn.getAttribute("data-category-fa") || btn.getAttribute("data-category") || btn.closest("[data-category-fa]")?.getAttribute("data-category-fa") || btn.closest("[data-category]")?.getAttribute("data-category") || null; if (!productId || !slug || !name || !Number.isFinite(price)) return; addItem({ productId, slug, name, price, image, category }); const isIcon = btn.classList.contains("card-add-btn"); if (isIcon) { btn.classList.add("is-added"); window.clearTimeout(Number(btn.getAttribute("data-added-timer") || 0)); const timer = window.setTimeout(() => { btn.classList.remove("is-added"); }, 1200); btn.setAttribute("data-added-timer", String(timer)); return; } const label = btn.getAttribute("data-label-default") || btn.textContent || ""; if (!btn.getAttribute("data-label-default")) { btn.setAttribute("data-label-default", label); } btn.textContent = "به سبد اضافه شد"; btn.classList.add("is-added"); window.clearTimeout(Number(btn.getAttribute("data-added-timer") || 0)); const timer = window.setTimeout(() => { btn.textContent = btn.getAttribute("data-label-default") || label; btn.classList.remove("is-added"); }, 1600); btn.setAttribute("data-added-timer", String(timer)); }); } /** Native share on PDP (clipboard fallback). */ export function initShareButtons() { document.addEventListener("click", async (event) => { const btn = event.target instanceof Element ? event.target.closest("[data-share-product]") : null; if (!(btn instanceof HTMLElement)) return; const title = btn.getAttribute("data-share-title") || document.title; const url = location.href; const defaultLabel = btn.getAttribute("data-label-default") || btn.textContent || "اشتراک‌گذاری"; if (!btn.getAttribute("data-label-default")) { btn.setAttribute("data-label-default", defaultLabel); } try { if (navigator.share) { await navigator.share({ title, url }); } else if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(url); btn.textContent = "لینک کپی شد"; btn.classList.add("is-shared"); window.clearTimeout(Number(btn.getAttribute("data-share-timer") || 0)); const timer = window.setTimeout(() => { btn.textContent = btn.getAttribute("data-label-default") || defaultLabel; btn.classList.remove("is-shared"); }, 1600); btn.setAttribute("data-share-timer", String(timer)); } } catch { /* user cancelled share sheet — ignore */ } }); } function escapeHtml(s) { return String(s ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } function displayName(p) { const variants = p.colorVariants; let title = p.name; if (variants && variants.length >= 2) { const shade = variants.find((v) => v.slug === p.slug)?.shade; if (shade) { const trimmed = p.name.trim(); if (trimmed.endsWith(shade)) title = trimmed.slice(0, -shade.length).trim(); } } const fa = p.categoryFa || null; if (fa && !title.includes(fa)) return `\u2067${fa} · \u2066${title}\u2069\u2069`; return title; } function shadeDots(p) { const variants = p.colorVariants; if (!variants || variants.length < 2) return ""; const shown = variants.slice(0, 5); const extra = variants.length - shown.length; return `
${shown .map((v) => { const active = v.slug === p.slug ? " is-active" : ""; const bg = v.heroImage ? `background-image:url('${escapeHtml(v.heroImage)}')` : ""; const href = `/products/${escapeHtml(v.slug)}/`; return ``; }) .join("")}${ extra > 0 ? `+${extra.toLocaleString("fa-IR")} عدد` : "" }
`; } function cardIsInStock(p) { const variants = p.colorVariants; if (variants && variants.length > 1) { return variants.some((v) => Number(v.stock) > 0); } return Number(p.stock) > 0; } function stockBadge(p) { if (cardIsInStock(p)) return ""; return `نا موجود`; } function cardHtml(p) { const title = displayName(p); const href = `/products/${escapeHtml(p.slug)}/`; const updated = p.updatedAt ? new Date(p.updatedAt).getTime() : 0; const sell = sellPriceFromItem(moneyItem(p, null)); const sellCompare = sellCompareFromItem(moneyItem(p, null)); const discount = sellCompare && sellCompare > sell ? Math.round((1 - sell / sellCompare) * 100) : 0; let bestDiscount = discount; for (const v of p.colorVariants || []) { const vs = sellPriceFromItem(moneyItem(p, v)); const vc = sellCompareFromItem(moneyItem(p, v)); if (vc && vc > vs) { bestDiscount = Math.max(bestDiscount, Math.round((1 - vs / vc) * 100)); } } const bounds = catalogPriceBounds(p); const priceMin = bounds.min; const priceMax = bounds.max; const attrs = [ `data-category="${escapeHtml(p.category || "")}"`, `data-category-fa="${escapeHtml(p.categoryFa || "")}"`, `data-price="${sell}"`, `data-price-min="${priceMin}"`, `data-price-max="${priceMax}"`, `data-name="${escapeHtml(p.name)}"`, `data-updated="${Number.isFinite(updated) ? updated : 0}"`, `data-rating="${p.ratingAvg || 0}"`, `data-reviews="${p.reviewCount || 0}"`, `data-featured="${p.featured ? 1 : 0}"`, `data-in-stock="${cardIsInStock(p) ? 1 : 0}"`, `data-discount="${bestDiscount}"`, ].join(" "); const badge = bestDiscount >= 1 ? `−${bestDiscount}%` : ""; const priceHtml = discount >= 1 ? `

${formatPrice(sell)} ${formatPrice(sellCompare)}

` : `

${formatPrice(sell)}

`; return `
${badge} ${stockBadge(p)} ${ p.heroImage ? `${escapeHtml(title)}` : "" }
${loveYourEyesPillHtml(p.slug)}

${escapeHtml(title)}

${shadeDots(p)}
${priceHtml}
`; } function catalogPriceBounds(p) { const prices = [sellPriceFromItem(moneyItem(p, null))]; for (const v of p.colorVariants || []) { const n = sellPriceFromItem(moneyItem(p, v)); if (Number.isFinite(n)) prices.push(n); } return { min: Math.min(...prices), max: Math.max(...prices) }; } function sortCatalog(products, sort) { const list = [...products]; switch (sort) { case "price-asc": return list.sort( (a, b) => catalogPriceBounds(a).min - catalogPriceBounds(b).min ); case "price-desc": return list.sort( (a, b) => catalogPriceBounds(b).max - catalogPriceBounds(a).max ); case "best-rated": return list.sort((a, b) => (b.ratingAvg || 0) - (a.ratingAvg || 0)); case "most-reviewed": return list.sort((a, b) => (b.reviewCount || 0) - (a.reviewCount || 0)); case "featured": return list.sort((a, b) => Number(b.featured) - Number(a.featured)); case "name-asc": return list.sort((a, b) => a.name.localeCompare(b.name, "fa")); case "newest": default: return list.sort( (a, b) => new Date(b.updatedAt) - new Date(a.updatedAt) ); } } function parseImagesAttr(raw, fallbackImage) { if (raw) { try { const parsed = JSON.parse(raw); if (Array.isArray(parsed) && parsed.length) { return parsed.filter((src) => typeof src === "string" && src); } } catch { /* ignore */ } } return fallbackImage ? [fallbackImage] : []; } function updatePdpDiscountBadge(discount) { document.querySelectorAll("[data-pdp-discount-badge]").forEach((el) => { if (!(el instanceof HTMLElement)) return; if (discount != null && discount >= 1) { el.textContent = `−${discount}%`; el.hidden = false; el.classList.add("offer-badge"); } else { el.textContent = ""; el.hidden = true; } }); } function pdpGalleryMarkup(images) { const gallery = images.filter((src) => typeof src === "string" && src); if (!gallery.length) return ""; return gallery .map( (src, i) => `` ) .join(""); } function splitMainAndRest(images, hero) { const all = (images || []).filter((src) => typeof src === "string" && src); const list = all.length ? all : hero ? [hero] : []; const main = hero || list[0] || ""; const rest = []; let skippedMain = false; for (const src of list) { if (!skippedMain && main && src === main) { skippedMain = true; continue; } rest.push(src); } return { main, rest }; } function updateVariantGallery(images, hero) { const { main, rest } = splitMainAndRest(images, hero); if (!main && !rest.length) return; const buyHeroImg = document.querySelector("[data-buy-hero-img]"); const buyHeroWrap = document.querySelector("[data-buy-hero]"); if (buyHeroImg && main) { buyHeroImg.setAttribute("src", main); if (buyHeroWrap instanceof HTMLElement) buyHeroWrap.hidden = false; } else if (buyHeroWrap instanceof HTMLElement) { buyHeroWrap.hidden = true; } const grids = document.querySelectorAll("[data-variant-gallery]"); if (grids.length) { const html = pdpGalleryMarkup(rest); grids.forEach((grid) => { grid.innerHTML = html; }); const shell = document.querySelector("[data-pdp-hero-wrap]"); if (shell instanceof HTMLElement) shell.hidden = !html; } else { const first = document.querySelector( ".pdp-gallery-shot img, .gallery-grid img, .pdp-hero img" ); if (first) first.setAttribute("src", main || rest[0] || ""); } const heroImg = document.querySelector(".pdp-hero img"); if (heroImg) heroImg.setAttribute("src", main || rest[0] || ""); } function applyShadeSelection(btn) { const panel = btn.closest("[data-buy-panel]") || document.querySelector("[data-buy-panel]"); if (!panel) return; const id = btn.getAttribute("data-id"); const slug = btn.getAttribute("data-slug"); const name = btn.getAttribute("data-name"); const shade = btn.getAttribute("data-shade") || ""; const intro = btn.getAttribute("data-intro") || ""; const price = Number(btn.getAttribute("data-price")); const compareRaw = btn.getAttribute("data-compare"); const compare = compareRaw ? Number(compareRaw) : null; const image = btn.getAttribute("data-image") || ""; const images = parseImagesAttr(btn.getAttribute("data-images"), image); const stock = Number(btn.getAttribute("data-stock") || 0); const low = Number(panel.getAttribute("data-low-stock") || 5); const labelAdd = panel.getAttribute("data-label-add") || "افزودن به سبد"; const labelOos = panel.getAttribute("data-label-oos") || "ناموجود"; const labelLimited = panel.getAttribute("data-label-limited") || ""; const labelSold = panel.getAttribute("data-label-sold") || ""; const shadeRoot = btn.closest("[data-shade-picker]") || document.querySelector("[data-pdp-shades] [data-shade-picker]") || panel; shadeRoot.querySelectorAll("[data-shade-select]").forEach((el) => { const on = el === btn; el.classList.toggle("is-active", on); el.setAttribute("aria-selected", on ? "true" : "false"); if (on) el.setAttribute("aria-current", "true"); else el.removeAttribute("aria-current"); }); const shadeLabel = shadeRoot.querySelector("[data-shade-label]") || panel.querySelector("[data-shade-label]"); if (shadeLabel) shadeLabel.textContent = shade; const title = panel.querySelector("[data-buy-title]"); if (title && name) title.textContent = name; const introEl = document.querySelector("[data-product-description-intro]"); if (introEl && intro) introEl.textContent = intro; const factsShade = document.querySelector('[data-product-fact="shade"]'); if (factsShade) factsShade.textContent = shade; if (name) document.title = `${name} | کیکومیکو`; const priceRow = panel.querySelector("[data-buy-price]"); if (priceRow && Number.isFinite(price)) { const discount = compare && compare > price ? Math.round(((compare - price) / compare) * 100) : null; priceRow.innerHTML = `${escapeHtml(formatPrice(price))}${ discount != null ? `${escapeHtml(formatPrice(compare))}−${discount}%` : "" }`; updatePdpDiscountBadge(discount); } const stockEl = panel.querySelector("[data-buy-stock]"); if (stockEl) { if (stock <= 0) stockEl.textContent = labelSold; else if (stock <= low) stockEl.textContent = `${labelLimited} ${stock}`; else stockEl.textContent = ""; } const syncCartBtn = (el) => { if (!(el instanceof HTMLElement)) return; el.setAttribute("data-product-id", id || ""); el.setAttribute("data-slug", slug || ""); el.setAttribute("data-name", name || ""); el.setAttribute("data-price", String(price)); el.setAttribute("data-image", image); const catFa = panel.getAttribute("data-category-fa") || panel.getAttribute("data-category") || ""; const cat = panel.getAttribute("data-category") || ""; if (catFa) el.setAttribute("data-category-fa", catFa); if (cat) el.setAttribute("data-category", cat); if (stock > 0) { el.removeAttribute("disabled"); if (!el.classList.contains("is-added")) { el.textContent = labelAdd; el.setAttribute("data-label-default", labelAdd); } } else { el.setAttribute("disabled", ""); el.textContent = labelOos; el.setAttribute("data-label-default", labelOos); } }; panel.querySelectorAll("[data-add-to-cart]").forEach(syncCartBtn); document .querySelectorAll(".sticky-buy [data-add-to-cart]") .forEach(syncCartBtn); const stickyName = document.querySelector(".sticky-buy [data-sticky-name]"); const stickyPrice = document.querySelector(".sticky-buy [data-sticky-price]"); if (stickyName && name) stickyName.textContent = name; if (stickyPrice && Number.isFinite(price)) stickyPrice.textContent = formatPrice(price); updateVariantGallery(images, image); if (shade) { const url = new URL(location.href); url.searchParams.set("shade", shade); history.replaceState( null, "", url.pathname + "?" + url.searchParams.toString() + url.hash ); } } export function initShadePicker() { const picker = document.querySelector("[data-shade-picker]"); if (!picker) return; picker.querySelectorAll("[data-shade-select]").forEach((btn) => { btn.addEventListener("click", () => applyShadeSelection(btn)); }); const params = new URLSearchParams(location.search); const wanted = params.get("shade"); if (!wanted) return; const match = [...picker.querySelectorAll("[data-shade-select]")].find( (el) => (el.getAttribute("data-shade") || "") === wanted ); if (match && !match.classList.contains("is-active")) { applyShadeSelection(match); } } /** Product-list shade dots: swap card image/price/link without navigating. */ export function initCardShadePickers() { document.addEventListener("click", (event) => { const target = event.target; if (!(target instanceof Element)) return; const btn = target.closest("[data-card-shade]"); if (!btn) return; event.preventDefault(); event.stopPropagation(); const card = btn.closest("[data-product-card]"); if (!card) return; const image = btn.getAttribute("data-image") || ""; const href = btn.getAttribute("data-href") || ""; const price = Number(btn.getAttribute("data-price")); const compareRaw = btn.getAttribute("data-compare"); const compare = compareRaw ? Number(compareRaw) : null; card.querySelectorAll("[data-card-shade]").forEach((el) => { const on = el === btn; el.classList.toggle("is-active", on); el.setAttribute("aria-selected", on ? "true" : "false"); }); const img = card.querySelector("[data-card-image]"); if (img && image) img.setAttribute("src", image); if (href) { card.querySelectorAll("[data-card-link]").forEach((link) => { link.setAttribute("href", href); }); } const priceEl = card.querySelector("[data-card-price]"); if (priceEl && Number.isFinite(price)) { card.setAttribute("data-price", String(price)); const addBtn = card.querySelector("[data-add-to-cart]"); if (addBtn) addBtn.setAttribute("data-price", String(price)); const discount = compare && compare > price ? Math.round(((compare - price) / compare) * 100) : 0; if (discount >= 1) { priceEl.classList.add("price--offer"); priceEl.innerHTML = `${escapeHtml(formatPrice(price))}${escapeHtml(formatPrice(compare))}`; } else { priceEl.classList.remove("price--offer"); priceEl.textContent = formatPrice(price); } const thumb = card.querySelector(".thumb"); let badge = card.querySelector(".offer-badge"); if (discount >= 1) { if (!badge && thumb) { badge = document.createElement("span"); badge.className = "offer-badge"; thumb.prepend(badge); } if (badge) badge.textContent = `−${discount}%`; card.classList.add("product-card--offer"); card.setAttribute("data-discount", String(discount)); } else { badge?.remove(); card.classList.remove("product-card--offer"); card.setAttribute("data-discount", "0"); } } }); } export function initSortForms() { const collection = document.querySelector("[data-static-collection]"); document.querySelectorAll("[data-sort-select]").forEach((select) => { select.addEventListener("change", async () => { // Client-mode listings sort in-place via filters.js — never hard-navigate. const listing = select.closest("[data-product-listing]"); if (listing?.getAttribute("data-client-mode") === "1") return; if (!collection) { const form = select.closest("form"); if (!(form instanceof HTMLFormElement)) return; const qs = new URLSearchParams(new FormData(form)); if (qs.get("sort") === "newest") qs.delete("sort"); const hash = form.getAttribute("data-hash"); location.assign( location.pathname + (qs.toString() ? `?${qs}` : "") + (hash ? `#${hash}` : "") ); return; } const grid = collection.querySelector("[data-product-grid]"); const countEl = collection.querySelector("[data-listing-count]") || collection.querySelector("[data-product-count]"); if (!grid) return; try { const res = await fetch("/data/catalog.dedupe2.json"); const data = await res.json(); const sorted = sortCatalog(data.products || [], select.value); const limit = Number(collection.getAttribute("data-home-limit") || 0); const shown = limit > 0 ? sorted.slice(0, limit) : sorted; grid.innerHTML = shown.map(cardHtml).join(""); if (countEl) countEl.textContent = shown.length.toLocaleString("fa-IR"); const pill = collection.querySelector("[data-filters-result-count]"); if (pill) pill.textContent = shown.length.toLocaleString("fa-IR"); const hash = select.closest("form")?.getAttribute("data-hash"); if (hash) location.hash = hash; initCardShadePickers(); } catch { /* keep existing grid */ } }); }); } const ROOT_SLUG_TO_CAT = { "make-up": "MAKE UP", "skin-care": "SKIN CARE", fragrances: "FRAGRANCES", accessories: "ACCESSORIES", }; function normalizeCatText(value) { return String(value || "") .toUpperCase() .replace(/[-\s]+/g, " ") .trim(); } function pathCategoryNeedle() { const path = location.pathname; let m = path.match(/\/category\/([^/]+)\/sub\/([^/]+)/); if (m) return { root: m[1], leaf: m[2], kind: "sub" }; m = path.match(/\/category\/([^/]+)\/type\/([^/]+)/); if (m) return { root: m[1], leaf: m[2], kind: "type" }; m = path.match(/\/category\/([^/]+)/); if (m) return { root: m[1], leaf: "", kind: "root" }; return null; } function slugifyCategorySegment(part) { return String(part || "") .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, ""); } function categoryPathParts(category) { return String(category || "") .split("/") .map((s) => s.trim()) .filter(Boolean); } function matchesCategoryRoot(p, rootSlug) { const lev1 = ROOT_SLUG_TO_CAT[rootSlug]; if (!lev1) return true; // Match first path segment only — avoids MAKE-UP BAG matching make-up. const first = normalizeCatText(categoryPathParts(p.category)[0] || ""); return first === normalizeCatText(lev1); } function productMatchesSubSlug(categoryRaw, subSlug) { if (!subSlug) return true; const leaf = subSlug.toLowerCase(); const slugParts = categoryPathParts(categoryRaw).map(slugifyCategorySegment); // Exact segment match (mid or leaf): eyes-make-up, face-skin-care, brushes, … if (slugParts.includes(leaf)) return true; // Simple pluralization on the last segment only (brush ↔ brushes). const last = slugParts[slugParts.length - 1] || ""; if (last === `${leaf}s` || leaf === `${last}s`) return true; const subNorm = normalizeCatText(subSlug.replace(/-/g, " ")); const catNorm = normalizeCatText(categoryRaw); // Full sub-label substring (not a short root prefix like "skin care"). if (subNorm.length >= 4 && catNorm.includes(subNorm)) return true; return false; } function productMatchesTypeSlug(p, typeSlug) { if (!typeSlug) return true; const parts = categoryPathParts(p.category); const lastSlug = slugifyCategorySegment(parts[parts.length - 1] || ""); const t = typeSlug.toLowerCase(); if ( lastSlug === t || lastSlug.startsWith(`${t}s`) || t.startsWith(lastSlug.replace(/s$/, "")) ) { return true; } const name = String(p.name || "").toLowerCase(); const keywords = { foundation: ["foundation", "skin tone", "skin tint"], concealer: ["concealer"], mascara: ["mascara"], "lip-gloss": ["lip gloss", "lipgloss", "lip oil"], blush: ["blush"], "eye-pencil": ["eye pencil", "kajal", "kohl"], }; const list = keywords[typeSlug]; return Boolean(list?.some((k) => name.includes(k))); } function productMatchesListing(p, listing, wrap) { const mode = listing?.getAttribute("data-listing-mode") || (wrap?.hasAttribute("data-home-limit") ? "home" : "") || ""; if (mode === "offers") { let best = 0; if (p.compareAtPrice && p.compareAtPrice > p.price) { best = Math.round((1 - p.price / p.compareAtPrice) * 100); } for (const v of p.colorVariants || []) { if (v.compareAtPrice && v.compareAtPrice > v.price) { best = Math.max( best, Math.round((1 - v.price / v.compareAtPrice) * 100) ); } } return best >= 30; } if (mode === "category") { const pathNeedle = pathCategoryNeedle(); const rootSlug = listing.getAttribute("data-category-slug") || pathNeedle?.root || ""; if (!matchesCategoryRoot(p, rootSlug)) return false; if (pathNeedle?.kind === "type" && pathNeedle.leaf) { return productMatchesTypeSlug(p, pathNeedle.leaf); } const urlLeaf = pathNeedle?.leaf || ""; if (urlLeaf) return productMatchesSubSlug(p.category, urlLeaf); const subLabel = listing.getAttribute("data-current-sub") || ""; if (subLabel && !/^[a-z0-9-]+$/i.test(subLabel)) { const labelNorm = normalizeCatText(subLabel); const catFa = normalizeCatText(p.categoryFa || ""); if (labelNorm && (normalizeCatText(p.category).includes(labelNorm) || catFa.includes(labelNorm))) { return true; } } return true; } return true; } /** Empty category exports ship without a grid — inject one before hydration. */ function ensureProductGrid(wrap, listing) { let grid = wrap.querySelector?.("[data-product-grid]") || listing?.closest?.(".page-pad")?.querySelector?.("[data-product-grid]"); if (grid) return grid; const pagePad = listing?.closest?.(".page-pad") || wrap.closest?.(".page-pad"); if (!pagePad) return null; pagePad.querySelectorAll(".container-luxury.section-padding").forEach((el) => { if (/محصولی در این دسته/.test(el.textContent || "")) el.remove(); }); grid = document.createElement("div"); grid.className = "container-luxury list-grid"; grid.setAttribute("data-product-grid", ""); const guide = pagePad.querySelector( "section.container-luxury.section-padding, section.section-padding, section.editorial-guide" ); if (guide) pagePad.insertBefore(grid, guide); else pagePad.appendChild(grid); return grid; } function megaHrefSubSlug(href) { const m = String(href || "").match(/\/sub\/([^/]+)/); return m?.[1]?.toLowerCase() || ""; } function countProductsForHref(products, href) { const rootMatch = String(href || "").match(/\/category\/([^/]+)/); const root = rootMatch?.[1] || ""; const sub = megaHrefSubSlug(href); return products.filter((p) => { if (!matchesCategoryRoot(p, root)) return false; return sub ? productMatchesSubSlug(p.category, sub) : true; }).length; } function faCount(n) { return `(${n.toLocaleString("fa-IR")})`; } /** Fill empty fragrance mega/mobile panes when catalog has FRAGRANCES products. */ function hydrateEmptyFragranceMenus(products) { const n = products.filter((p) => matchesCategoryRoot(p, "fragrances")).length; if (n < 1) return; const countText = faCount(n); document .querySelectorAll('.mega-pane[data-mega-pane="fragrances"] .mega-children') .forEach((ul) => { const empty = [...ul.querySelectorAll("li.text-muted")].find((li) => /هنوز محصولی/.test(li.textContent || "") ); if (!empty) return; empty.outerHTML = `
  • |01| عطر${countText}
  • `; }); document .querySelectorAll('.mobile-sub[data-mobile-sub="fragrances"]') .forEach((sub) => { if (sub.querySelector("a[href*='/category/fragrances']")) return; sub.innerHTML = `عطر ${countText}`; const row = sub.closest(".mobile-cat")?.querySelector(".mobile-cat-row"); if (row && !row.querySelector("[data-mobile-expand]")) { const btn = document.createElement("button"); btn.type = "button"; btn.setAttribute("data-mobile-expand", "fragrances"); btn.setAttribute("aria-label", "زیرمجموعه‌های عطر"); btn.innerHTML = ''; row.appendChild(btn); } }); } /** Refresh (0) placeholders in the mega + mobile menus from the live catalog. */ function hydrateMegaMenuCounts(products) { hydrateEmptyFragranceMenus(products); document.querySelectorAll(".mega-child a[href*='/category/']").forEach((link) => { if (!(link instanceof HTMLAnchorElement)) return; const countEl = link.querySelector(".mega-count"); if (!countEl) return; const n = countProductsForHref(products, link.getAttribute("href")); countEl.textContent = faCount(n); }); // Mobile drawer uses bare (0) without .mega-count document.querySelectorAll(".mobile-sub a[href*='/category/']").forEach((link) => { if (!(link instanceof HTMLAnchorElement)) return; const span = link.querySelector("span"); if (!span) return; const n = countProductsForHref(products, link.getAttribute("href")); span.textContent = faCount(n); }); // Filter sheet data-count attrs (used by estimateCount fallback) document .querySelectorAll('[data-filter-option="category"][data-count]') .forEach((btn) => { if (!(btn instanceof HTMLElement)) return; const href = btn.getAttribute("data-href") || ""; if (!href.includes("/category/")) return; const n = countProductsForHref(products, href); btn.setAttribute("data-count", String(n)); const countSpan = btn.querySelector(".filters-option-count"); if (countSpan) countSpan.textContent = n.toLocaleString("fa-IR"); }); } /** * Design-export pages ship empty product grids. Hydrate them from * /data/catalog.dedupe2.json so the CMS export actually appears. * Mega-menu counts always refresh — even when grids already have cards * (homepage) — so (0) placeholders are not left behind after a catalog swap. */ export async function initStaticCatalogHydration() { await ensureEurRate(); const wraps = [ ...document.querySelectorAll("[data-static-collection]"), ...document.querySelectorAll( '[data-product-listing][data-client-mode="1"]' ), ]; const targets = []; const seen = new Set(); for (const el of wraps) { const wrap = el.closest(".collection-wrap") || el.closest(".page-pad") || el.parentElement || el; let grid = wrap.querySelector?.("[data-product-grid]") || el.querySelector?.("[data-product-grid]"); const listing = wrap.querySelector?.("[data-product-listing]") || el.closest?.("[data-product-listing]") || el; if (!grid && listing?.getAttribute("data-listing-mode") === "category") { grid = ensureProductGrid(wrap, listing); } if (!grid || seen.has(grid)) continue; if (grid.querySelector("[data-product-card]")) continue; // Search page has its own loader if (grid.hasAttribute("data-search-results")) continue; seen.add(grid); targets.push({ wrap, grid, listing }); } let products = []; try { const res = await fetch("/data/catalog.dedupe2.json"); if (!res.ok) throw new Error("catalog fetch failed"); const data = await res.json(); products = data.products || []; } catch { return; } // Always refresh menu counts from the live catalog (homepage included). hydrateMegaMenuCounts(products); if (!targets.length) return; for (const { wrap, grid, listing } of targets) { const sort = listing?.getAttribute("data-current-sort") || new URLSearchParams(location.search).get("sort") || "newest"; let list = products.filter((p) => productMatchesListing(p, listing, wrap) ); list = sortCatalog(list, sort); const limit = Number( wrap.getAttribute?.("data-home-limit") || listing?.closest?.("[data-home-limit]")?.getAttribute("data-home-limit") || 0 ); const shown = limit > 0 ? list.slice(0, limit) : list; grid.innerHTML = shown.map(cardHtml).join(""); const countEl = listing?.querySelector?.("[data-listing-count]") || wrap.querySelector?.("[data-listing-count]"); if (countEl) countEl.textContent = shown.length.toLocaleString("fa-IR"); const pill = listing?.querySelector?.("[data-filters-result-count]"); if (pill) pill.textContent = shown.length.toLocaleString("fa-IR"); } initCardShadePickers(); } export { formatPrice };