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
? `
`
: ""
}
`;
}
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 };