/* Add custom JS code below */
(function () {
"use strict";
// Language detection: relies solely on / dir attribute
var LANG = {
detect: function () {
var htmlLang = (
document.documentElement.getAttribute("lang") || ""
).toLowerCase();
if (htmlLang.indexOf("en") === 0) return "en";
if (htmlLang.indexOf("ar") === 0) return "ar";
var dir = (
document.documentElement.getAttribute("dir") || ""
).toLowerCase();
if (dir === "ltr") return "en";
return "ar";
},
isAr: function () {
return this.detect() === "ar";
},
t: function (arText, enText) {
return this.isAr() ? arText : enText;
},
};
// ==================== SHARED API HEADERS ====================
// Every Salla API call MUST carry the current page language, otherwise
// the API silently falls back to Arabic (this is what was causing the
// "data always comes in Arabic" issue). extra lets a specific call add
// headers on top of the shared base without duplicating this block.
function buildApiHeaders(extra) {
var lang = LANG.detect(); // "ar" or "en"
var headers = {
accept: "application/json, text/plain, */*",
currency: "SAR",
"accept-language": lang,
lang: lang,
origin: window.location.origin,
"store-identifier": "1761887103",
"x-requested-with": "XMLHttpRequest",
};
if (extra) {
for (var k in extra) headers[k] = extra[k];
}
return headers;
}
var CONFIG = {
MAX_TRIES: 50,
CHECK_INTERVAL: 500,
INJECTED_CLASS: "roani-card-opt-injected",
VARIANT_CLASS: "roani-variants-injected",
BUNDLES_CLASS: "roani-bundles-injected",
USP_CLASS: "roani-usp-injected",
TITLE_ROW_CLASS: "roani-title-row-injected",
PRODUCTS_API: "https://api.salla.dev/store/v1/products/options",
COMMENTS_API: "https://api.salla.dev/store/v1/comments",
RATING_SUMMARY_API: "https://api.salla.dev/store/v1/rating/summary/",
STORE_ID: "210732775",
BUNDLE_IDS: ["1557700897", "1143856518"],
// Desired display order for "روائح مشابهة" (right-to-left reading order):
// سيرينا -> سولا -> قلاسيلا -> سوفيا -> قستيا
VARIANT_ORDER: [
"1344233220",
"1756892624",
"1528707975",
"1474857014",
"769327947",
],
VIDEO_SECTION_SELECTOR: "#slider_videos-",
BADGES: [
{ icon: "🌿", text_ar: "عضوية وطبيعية", text_en: "Organic & Natural" },
{ icon: "🕯️", text_ar: "فتيل 100% قطن", text_en: "100% Cotton Wick" },
],
};
// NOTE: description / notes / feeling below are custom marketing copy
// that does NOT exist anywhere in Salla's product API response (the API
// only returns "description" as a raw HTML/image blob, no "notes" or
// "feeling" fields at all). So these three stay hardcoded on purpose —
// sending accept-language won't ever produce them. Only `name` and
// `subtitle` are real Salla fields, and those now come localized
// straight from the API once the language headers are sent, so the
// BUNDLE_NAMES_EN / BUNDLE_SUBTITLES_EN hacks below are kept only as a
// safety-net fallback in case a specific bundle response is missing one.
var BUNDLE_NAMES_EN = {
1557700897: "For All Your Moments",
1143856518: "For Your Important Moment",
};
var BUNDLE_SUBTITLES_EN = {
1557700897:
"A harmony that follows you from first light to the calm of night",
1143856518: "A warm, gentle scent that flows with the calm of the moment",
};
var STATIC_DATA = [
{
id: 1528707975,
name_ar: "قلاسيلا",
name_en: "Glacella",
description: "لحظة هدوء تحت أحضان الطبيعة.",
description_en: "A calm moment embraced by nature.",
notes: ["خشب الصندل","العسل", "الياسمين", "زهور فاكهية"],
notes_en: ["Sandalwood", "Honey", "Jasmine", "Fruity Blooms"],
feeling: "السكون في حديقة عند وقت العصر",
feeling_en: "Stillness in a garden at dawn",
},
{
id: 1344233220,
name_ar: "سيرينا",
name_en: "Serena",
description: "حلم وردي يوقظ صباحك بلطف.",
description_en: "A rosy dream that gently wakes your morning.",
notes: ["البرغموت", "البنفسج", "الباتشولي", "خشب الصندل"],
notes_en: ["Bergamot", "Apple", "Berries", "Sandalwood"],
feeling: "بسمة فجر بعد ليلة طويلة",
feeling_en: "A dawn smile after a long night",
},
{
id: 1474857014,
name_ar: "سوفيا",
name_en: "Sovea",
description: "دفء يجمع القلوب عند الغروب.",
description_en: "Warmth that brings hearts together at sunset.",
notes: ["الفلفل الوردي", "اللافندر", "المسك", "الفانيلا"],
notes_en: ["Pink Pepper", "Lavender", "Musk", "Vanilla"],
feeling: "دفء وقت الغروب",
feeling_en: "Warmth at sunset",
},
{
id: 1756892624,
name_ar: "سولا",
name_en: "Sola",
description: "إشراقة تملأ صباحك بالطاقة.",
description_en: "A radiance that fills your morning with energy.",
notes: ["خشب الصندل", "كاكاو", "ياسمين"],
notes_en: ["Sandalwood", "Cocoa", "Jasmine"],
feeling: "شمس الصباح على شرفة دافئة",
feeling_en: "Morning sun on a warm balcony",
},
{
id: 769327947,
name_ar: "قستيا",
name_en: "Gustea",
description: "سكون ناعم في نهاية اليوم.",
description_en: "A gentle stillness at the end of the day.",
notes: ["الجلد", "المسك", "الفريزيا", "لمسة خشبية"],
notes_en: ["Freesia", "Musk", "Leather", "Wooden Touch"],
feeling: "نهاية يوم طويل بهدوء تام",
feeling_en: "The calm end of a long day",
},
];
function getStaticData(productId) {
for (var i = 0; i < STATIC_DATA.length; i++) {
if (String(STATIC_DATA[i].id) === String(productId))
return STATIC_DATA[i];
}
return null;
}
// Sorts products by a fixed list of ids. Any product whose id isn't in
// orderIds is left out of the sort and appended at the end (in its
// original relative order), so new/unlisted products never disappear —
// they just land last instead of breaking the layout.
function sortByIdOrder(products, orderIds) {
var orderIndex = {};
orderIds.forEach(function (id, idx) {
orderIndex[String(id)] = idx;
});
var known = [];
var unknown = [];
products.forEach(function (p) {
if (orderIndex.hasOwnProperty(String(p.id))) known.push(p);
else unknown.push(p);
});
known.sort(function (a, b) {
return orderIndex[String(a.id)] - orderIndex[String(b.id)];
});
return known.concat(unknown);
}
function isArabicText(str) {
return /[\u0600-\u06FF]/.test(str);
}
function isLatinText(str) {
return /[A-Za-z]/.test(str);
}
// Splits "English - عربي" style names and returns the part matching current language
// Kept as a fallback for the rare case the API still returns a mixed string.
function cleanProductName(rawName) {
if (!rawName) return "";
var name = String(rawName).trim();
var wantArabic = LANG.isAr();
var separators = [" - ", " – ", " — ", "-", "–", "—"];
for (var i = 0; i < separators.length; i++) {
var sep = separators[i];
var idx = name.indexOf(sep);
if (idx !== -1) {
var parts = name.split(sep);
var j;
for (j = 0; j < parts.length; j++) {
var part = parts[j].trim();
if (!part) continue;
if (wantArabic && isArabicText(part)) return part;
if (!wantArabic && isLatinText(part) && !isArabicText(part))
return part;
}
return wantArabic ? parts[parts.length - 1].trim() : parts[0].trim();
}
}
return name;
}
function getDisplayName(product) {
// 1) Trust the API first — with the lang header sent, product.name
// should already be in the right language.
if (product && product.name) {
if (!isArabicText(product.name) === !LANG.isAr()) {
// language of the string doesn't match what we expect -> mixed/legacy string, clean it
return cleanProductName(product.name);
}
return product.name.trim();
}
// 2) Fallback to static / hardcoded bundle names if the API gave nothing usable
var staticData = getStaticData(product.id);
if (staticData)
return LANG.isAr() ? staticData.name_ar : staticData.name_en;
if (!LANG.isAr() && BUNDLE_NAMES_EN[String(product.id)])
return BUNDLE_NAMES_EN[String(product.id)];
return cleanProductName(product.name);
}
function getDisplaySubtitle(product) {
// Trust the API's localized subtitle first, fall back to hardcoded EN copy.
if (product && product.subtitle) return product.subtitle;
if (!LANG.isAr() && BUNDLE_SUBTITLES_EN[String(product.id)]) {
return BUNDLE_SUBTITLES_EN[String(product.id)];
}
return product.subtitle;
}
var state = {
isInjected: false,
uspInjected: false,
titleRowInjected: false,
variantInjected: false,
bundlesInjected: false,
tries: 0,
uspTries: 0,
titleRowTries: 0,
variantTries: 0,
bundlesTries: 0,
interval: null,
uspInterval: null,
titleRowInterval: null,
variantInterval: null,
bundlesInterval: null,
currentProductId: null,
// Whether the page we're currently on IS a bundle's own product page.
// Computed once (on first successful injectVariants() run) and kept
// fixed for the lifetime of the page — switching between bundles via
// the cards below never changes what page we're physically on.
isBundlePage: null,
variantsData: [],
currentVariantIds: [],
};
// ==================== INJECT (UI fixes) ====================
function inject() {
if (document.querySelector("." + CONFIG.INJECTED_CLASS)) return;
var thumbSlides = document.querySelectorAll(
".s-slider-thumbs .swiper-slide",
);
var wishlistBar = document.querySelector(".main-content .shadow-sm");
if (!thumbSlides.length || !wishlistBar) return;
var marker = document.createElement("span");
marker.className = CONFIG.INJECTED_CLASS;
marker.style.display = "none";
document.body.appendChild(marker);
thumbSlides.forEach(function (slide) {
slide.style.cssText +=
"aspect-ratio: 1 / 1 !important; height: auto !important;";
});
var styleEl = document.createElement("style");
styleEl.textContent = [
".s-slider-thumbs .swiper-slide { aspect-ratio: 1 / 1 !important; }",
".s-slider-thumbs .swiper-slide .lazy__bg { background-size: cover !important; background-position: center !important; }",
].join(" ");
document.head.appendChild(styleEl);
wishlistBar.style.display = "none";
cleanMainProductTitle();
state.isInjected = true;
}
function cleanMainProductTitle() {
var titleEl = document.querySelector(".product-title");
if (!titleEl) return;
var currentId = state.currentProductId || getCurrentProductId();
var staticData = currentId ? getStaticData(currentId) : null;
if (staticData) {
titleEl.textContent = LANG.isAr()
? staticData.name_ar
: staticData.name_en;
return;
}
if (!LANG.isAr() && currentId && BUNDLE_NAMES_EN[String(currentId)]) {
titleEl.textContent = BUNDLE_NAMES_EN[String(currentId)];
return;
}
var cleaned = cleanProductName(titleEl.textContent);
if (cleaned && cleaned !== titleEl.textContent.trim()) {
titleEl.textContent = cleaned;
}
}
// ==================== USP BADGES ====================
function injectUSP() {
var existing = document.querySelector("." + CONFIG.USP_CLASS);
if (existing) existing.remove();
var titleEl = document.querySelector(".product-title");
if (!titleEl) return;
var row = document.createElement("div");
row.className = CONFIG.USP_CLASS;
row.style.cssText =
"display:flex;gap:10px;flex-wrap:wrap;margin:10px 0 14px;direction:" +
(LANG.isAr() ? "rtl" : "ltr") +
";";
CONFIG.BADGES.forEach(function (badge) {
var span = document.createElement("span");
span.style.cssText =
"display:inline-flex;align-items:center;gap:7px;padding:5px 14px;border-radius:999px;border:0.5px solid #dccfbc;background:#ffffff;color:#4a403a;font-size:13px;font-weight:500;line-height:1.4;";
var label = LANG.isAr() ? badge.text_ar : badge.text_en;
span.innerHTML =
"" +
badge.icon +
" " +
label +
" ";
row.appendChild(span);
});
titleEl.insertAdjacentElement("afterend", row);
state.uspInjected = true;
}
// ==================== TITLE ROW ====================
function injectTitleRow() {
if (document.querySelector("." + CONFIG.TITLE_ROW_CLASS)) return;
var titleEl = document.querySelector(".product-title");
if (!titleEl) return;
var staticWrap = document.querySelector(".main-content-static");
if (!staticWrap) return;
var shareBar = staticWrap.querySelector(".shadow-sm");
if (!shareBar) return;
shareBar.className = "roani-share-bar";
shareBar.style.cssText = "";
var titleParent = titleEl.parentNode;
var wrapper = document.createElement("div");
wrapper.className = CONFIG.TITLE_ROW_CLASS;
wrapper.style.cssText = [
"display:flex",
"align-items:center",
"justify-content:space-between",
"gap:12px",
"direction:" + (LANG.isAr() ? "rtl" : "ltr"),
"width:100%",
].join(";");
var titleWrap = document.createElement("div");
titleWrap.style.cssText = "flex:1;min-width:0;";
titleWrap.appendChild(titleEl.cloneNode(true));
wrapper.appendChild(titleWrap);
wrapper.appendChild(shareBar);
titleParent.replaceChild(wrapper, titleEl);
state.titleRowInjected = true;
}
// ==================== HELPERS ====================
function getCurrentProductId() {
var comp = document.querySelector(
"show-home-component[component-path='saji-photo-gallery']",
);
if (comp) return comp.getAttribute("current-prod-id");
var form = document.querySelector("form.product-form");
if (!form) return null;
var addBtn = form.querySelector("[product-id]");
return addBtn ? addBtn.getAttribute("product-id") : null;
}
// Best-effort resolver for a product's own page URL, used when we need
// to navigate (full page load) to a bundle instead of updating in place.
function getProductUrl(p) {
if (!p) return null;
if (p.url) return p.url;
if (p.urls && p.urls.customer) return p.urls.customer;
if (p.urls && p.urls.customer_url) return p.urls.customer_url;
return null;
}
function isEmptyParagraphEl(el) {
if (!el || el.nodeType !== 1 || el.tagName !== "P") return false;
var html = el.innerHTML.trim().toLowerCase();
var text = el.textContent.trim();
return (html === " " || html === "" || html === " ") && text === "";
}
function removeConsecutiveEmptyParagraphsInBody(container) {
if (!container) return;
var children = Array.prototype.slice.call(container.children);
var previousWasEmpty = false;
for (var i = 0; i < children.length; i++) {
var el = children[i];
var currentIsEmpty = isEmptyParagraphEl(el);
if (currentIsEmpty && previousWasEmpty) {
el.remove();
} else {
previousWasEmpty = currentIsEmpty;
}
}
}
function fetchVariantsData(ids, callback) {
if (!ids.length) return;
var params = ids.map(function (id) {
return "selected[]=" + id;
});
params.push("with[]=images");
var url = CONFIG.PRODUCTS_API + "?" + params.join("&");
fetch(url, {
method: "GET",
headers: buildApiHeaders(),
})
.then(function (res) {
return res.json();
})
.then(function (json) {
if (json && json.data) callback(json.data);
})
.catch(function (err) {
console.error("Variants fetch error:", err);
});
}
// ==================== SMOOTH SCROLL ====================
function smoothScrollToTop() {
var target =
document.querySelector(".s-slider-container") ||
document.querySelector(".product-entry") ||
document.querySelector(".main-content");
if (target) {
var headerOffset = 80;
var elementPosition = target.getBoundingClientRect().top;
var offsetPosition = elementPosition + window.pageYOffset - headerOffset;
window.scrollTo({ top: offsetPosition, behavior: "smooth" });
} else {
window.scrollTo({ top: 0, behavior: "smooth" });
}
}
// ==================== PRICE BLOCK ====================
function readCurrentPrices() {
var salePriceEl = document.querySelector(".total-price-single");
var beforePriceEl = document.querySelector(".before-price-single");
var saleText = salePriceEl
? salePriceEl.textContent.trim().replace(/[^\d.]/g, "")
: "";
var beforeText = beforePriceEl
? beforePriceEl.textContent.trim().replace(/[^\d.]/g, "")
: "";
return { sale: saleText, before: beforeText };
}
function buildPriceBlock(salePrice, regularPrice) {
if (!salePrice || salePrice === "" || isNaN(parseFloat(salePrice)))
return false;
var existing = document.querySelector(".roani-price-block");
if (existing) existing.remove();
var salePriceEl = document.querySelector(".total-price-single");
var existingPriceWrap = salePriceEl
? salePriceEl.closest(".flex.flex-wrap")
: null;
if (!existingPriceWrap) return false;
var currencyLabel = LANG.isAr() ? "ر.س" : "SAR";
var saveWord = LANG.isAr() ? "وفر" : "Save";
var offWord = LANG.isAr() ? "خصم" : "Off";
var taxWord = LANG.isAr() ? "شامل الضريبة" : "Tax Included";
var saleNum = parseFloat(salePrice);
var regularNum = regularPrice ? parseFloat(regularPrice) : null;
var saveAmount =
regularNum && saleNum && regularNum > saleNum
? regularNum - saleNum
: null;
var discountPct = saveAmount
? Math.round((saveAmount / regularNum) * 100)
: null;
var newBlock = document.createElement("div");
newBlock.className = "roani-price-block";
newBlock.innerHTML =
'' +
salePrice +
" " +
currencyLabel +
" " +
(regularNum && regularNum !== saleNum
? '' +
regularPrice +
" " +
currencyLabel +
" "
: "") +
(saveAmount
? '' +
saveWord +
" " +
saveAmount +
" • " +
offWord +
" " +
discountPct +
"% "
: "") +
'' +
taxWord +
" ";
existingPriceWrap.style.display = "none";
var taxEl = document.querySelector("small.text-red-400");
if (taxEl) taxEl.style.display = "none";
existingPriceWrap.parentNode.insertBefore(newBlock, existingPriceWrap);
return true;
}
// ==================== STATIC INFO ====================
function buildStaticInfoBlock(staticData) {
var existing = document.querySelector(".roani-static-info");
if (existing) existing.remove();
if (!staticData) return;
var starsEl = document.querySelector(
"form.product-form salla-rating-stars",
);
var ratingRow = starsEl ? starsEl.closest(".flex.items-center.mb-4") : null;
var container = ratingRow
? ratingRow.parentElement
: document.querySelector("form.product-form .flex.flex-col");
if (!container) return;
var insertAfter =
ratingRow ||
(container.children.length > 0 ? container.children[0] : null);
var isAr = LANG.isAr();
var block = document.createElement("div");
block.className = "roani-static-info";
var descEl = document.createElement("p");
descEl.className = "roani-short-desc";
descEl.textContent = isAr
? staticData.description
: staticData.description_en;
block.appendChild(descEl);
var rows = [
{
label: isAr ? "المكونات" : "Notes",
value: (isAr ? staticData.notes : staticData.notes_en).join(
isAr ? "، " : ", ",
),
},
{
label: isAr ? "الإحساس" : "Feeling",
value: isAr ? staticData.feeling : staticData.feeling_en,
},
];
var table = document.createElement("div");
table.className = "roani-info-table";
rows.forEach(function (row) {
var rowEl = document.createElement("div");
rowEl.className = "roani-info-row";
var labelEl = document.createElement("span");
labelEl.className = "roani-info-label";
labelEl.textContent = row.label;
var valueEl = document.createElement("span");
valueEl.className = "roani-info-value";
valueEl.textContent = row.value;
rowEl.appendChild(labelEl);
rowEl.appendChild(valueEl);
table.appendChild(rowEl);
});
block.appendChild(table);
if (insertAfter && insertAfter.nextSibling) {
container.insertBefore(block, insertAfter.nextSibling);
} else if (insertAfter) {
container.appendChild(block);
} else {
container.prepend(block);
}
}
// ==================== SALLA CART ADD ====================
function sallaCartAdd(productId, btnEl) {
return window.salla.cart
.addItem({
id: Number(productId),
quantity: 1,
})
.then(function (res) {
btnEl.classList.remove("is-loading");
btnEl.classList.add("is-success");
setTimeout(function () {
btnEl.classList.remove("is-success");
btnEl.disabled = false;
}, 2000);
return res;
})
.catch(function (err) {
console.error("salla.cart.addItem error:", err);
btnEl.classList.remove("is-loading");
btnEl.classList.add("is-error");
btnEl.disabled = false;
setTimeout(function () {
btnEl.classList.remove("is-error");
}, 1800);
throw err;
});
}
// ==================== FETCH COMMENTS ====================
function fetchComments(productId, callback) {
var url =
CONFIG.COMMENTS_API +
"?per_page=5&page=1&type=products&ids[]=" +
productId;
fetch(url, {
method: "GET",
headers: buildApiHeaders(),
})
.then(function (res) {
return res.json();
})
.then(function (json) {
if (json && json.data) callback(json.data, json.pagination);
})
.catch(function (err) {
console.error("Comments fetch error:", err);
});
}
// ==================== RATING SUMMARY ====================
function fetchRatingSummary(productId, callback) {
var url = CONFIG.RATING_SUMMARY_API + productId;
fetch(url, {
method: "GET",
headers: buildApiHeaders(),
})
.then(function (res) {
return res.json();
})
.then(function (json) {
if (json && json.data) callback(json.data);
})
.catch(function (err) {
console.error("Rating summary fetch error:", err);
});
}
function updateRatingSummary(ratingData) {
var starsEl = document.querySelector(
"form.product-form salla-rating-stars",
);
if (!starsEl) return;
if (ratingData.rating != null) {
starsEl.setAttribute("value", ratingData.rating);
var starSpans = starsEl.querySelectorAll(".s-rating-stars-btn-star");
var filled = Math.round(ratingData.rating);
starSpans.forEach(function (star, idx) {
if (idx < filled) star.classList.add("s-rating-stars-selected");
else star.classList.remove("s-rating-stars-selected");
});
}
var ratingCountEl = starsEl.parentElement
? starsEl.parentElement.querySelector("strong")
: null;
if (ratingCountEl && ratingData.count != null) {
var totalDisplay = LANG.isAr()
? toArabicNumerals(ratingData.count)
: ratingData.count;
var suffix = LANG.isAr() ? "تقييم" : "ratings";
ratingCountEl.textContent = "(" + totalDisplay + ") " + suffix;
}
}
function updateComments(comments, pagination) {
var sallaComments = document.querySelector("salla-comments");
if (!sallaComments) return;
var countLabel = sallaComments.querySelector(".s-comments-count-label");
if (countLabel && pagination) {
var totalDisplay = LANG.isAr()
? toArabicNumerals(pagination.total)
: pagination.total;
var suffix = LANG.isAr() ? "تعليق" : "comments";
countLabel.innerHTML = " " + totalDisplay + " " + suffix;
}
var firstItem = sallaComments.querySelector("salla-comment-item");
if (!firstItem) return;
var itemsParent = firstItem.parentNode;
var existingItems = itemsParent.querySelectorAll("salla-comment-item");
existingItems.forEach(function (el) {
el.remove();
});
comments.forEach(function (comment) {
var item = buildCommentItem(comment);
itemsParent.appendChild(item);
});
}
function buildCommentItem(comment) {
var wrapper = document.createElement("salla-comment-item");
wrapper.className = "s-comments-item hydrated animated";
wrapper.style.cssText = "opacity: 1; transform: translateY(0px);";
function buildStars(rating) {
var html = '
';
for (var i = 1; i <= 5; i++) {
var selected = i <= rating ? " s-rating-stars-selected" : "";
html +=
'
' +
'' +
' ' +
" ";
}
html += "
";
return html;
}
function formatDate(createdAt) {
try {
var d = new Date(createdAt.date.replace(" ", "T"));
var day = d.getDate();
var month = d.getMonth() + 1;
var year = d.getFullYear();
var hours = d.getHours();
var minutes = d.getMinutes();
return (
day +
"/" +
month +
"/" +
year +
'"
);
} catch (e) {
return "";
}
}
var imagesHtml = "";
if (comment.images && comment.images.length) {
comment.images.forEach(function (imgUrl) {
imagesHtml += ' ';
});
}
var checkSvg =
' ';
var hasOrderText = LANG.isAr() ? "قام بالشراء , " : "Verified purchase, ";
var ratedText = LANG.isAr() ? "تم التقييم" : "Rated";
wrapper.innerHTML = [
'",
].join("");
// salla-rating-stars hydrates asynchronously and injects its own
// interactive stars wrapper on top of ours — strip it, once now and
// a few more times shortly after in case hydration hasn't run yet.
var starsEl = wrapper.querySelector(
"salla-rating-stars.s-comments-item-stars",
);
stripInteractiveStars(starsEl);
var starsCleanTries = 0;
var starsCleanInterval = setInterval(function () {
starsCleanTries++;
stripInteractiveStars(starsEl);
if (starsCleanTries >= 10) clearInterval(starsCleanInterval);
}, 200);
return wrapper;
}
// The custom element hydrates itself and injects its
// own interactive "pick a rating" widget (buttons + hidden input) as a
// second ".s-rating-stars-wrapper" alongside the display-only stars we
// build ourselves. That second wrapper is never the one with our
// "-mini"/"-selected" star spans, so we can safely detect and remove it.
function stripInteractiveStars(starsEl) {
if (!starsEl) return;
var wrappers = starsEl.querySelectorAll(".s-rating-stars-wrapper");
wrappers.forEach(function (w) {
if (!w.querySelector(".s-rating-stars-mini")) {
w.remove();
}
});
}
function toArabicNumerals(num) {
return String(num).replace(/\d/g, function (d) {
return "٠١٢٣٤٥٦٧٨٩"[d];
});
}
function refreshRatingsAndComments(productId) {
fetchComments(productId, function (comments, pagination) {
if (String(state.currentProductId) !== String(productId)) return;
updateComments(comments, pagination);
});
fetchRatingSummary(productId, function (ratingData) {
if (String(state.currentProductId) !== String(productId)) return;
updateRatingSummary(ratingData);
});
}
// ==================== VIDEO SECTION ("ماذا يأتي مع شمعة ...") ====================
// The API's `images` array sometimes contains a video entry mixed in with
// normal images: { type: "video", url: , video_url: }.
// We scan for that entry, tell it apart from real images via type==="video",
// and swap it into the video slider section, updating the section title too.
function findVideoInImages(images) {
if (!images || !images.length) return null;
for (var i = 0; i < images.length; i++) {
if (images[i] && images[i].type === "video" && images[i].video_url) {
return images[i];
}
}
return null;
}
function extractYouTubeId(url) {
if (!url) return null;
var match = url.match(
/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([A-Za-z0-9_-]{6,})/,
);
return match ? match[1] : null;
}
function buildVideoSectionTitle(name) {
return LANG.isAr()
? "ماذا يأتي مع شمعة " + name
: "What Comes With " + name + " Candle";
}
function updateVideoSection(product) {
var section = document.querySelector(CONFIG.VIDEO_SECTION_SELECTOR);
if (!section) return;
var displayName = getDisplayName(product);
var titleEl = section.querySelector(".saji-main-title");
if (titleEl) titleEl.textContent = buildVideoSectionTitle(displayName);
var slideItem = section.querySelector(".slider_videos-item");
if (!slideItem) return;
var videoData = findVideoInImages(product.images);
if (!videoData) {
// No video for this product: hide the whole section rather than show stale media.
section.style.display = "none";
return;
}
section.style.display = "";
var ytId = extractYouTubeId(videoData.video_url);
var existingVideoEl = slideItem.querySelector("video");
var existingIframeEl = slideItem.querySelector("iframe.roani-video-embed");
if (ytId) {
// YouTube link: a native tag can't play it, use an embed iframe instead.
if (existingVideoEl)
existingVideoEl.parentNode.removeChild(existingVideoEl);
var iframe = existingIframeEl;
if (!iframe) {
iframe = document.createElement("iframe");
iframe.className =
"roani-video-embed w-full h-[350px] lg:h-[550px] object-cover";
iframe.setAttribute("frameborder", "0");
iframe.setAttribute(
"allow",
"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
);
iframe.setAttribute("allowfullscreen", "");
slideItem.appendChild(iframe);
}
iframe.src = "https://www.youtube.com/embed/" + ytId;
} else {
// Direct video file: reuse/rebuild a normal tag.
if (existingIframeEl)
existingIframeEl.parentNode.removeChild(existingIframeEl);
var videoEl = existingVideoEl;
if (!videoEl) {
videoEl = document.createElement("video");
videoEl.controls = true;
videoEl.width = 400;
videoEl.className = "w-full h-[350px] lg:h-[550px] object-cover lazy";
slideItem.appendChild(videoEl);
}
videoEl.src = videoData.video_url;
videoEl.setAttribute("data-src", videoData.video_url);
}
}
// ==================== UPDATE PAGE ====================
function updatePageWithProduct(product) {
var cleanName = getDisplayName(product);
document.querySelectorAll(".product-title").forEach(function (el) {
el.textContent = cleanName;
});
var subtitleEl = document.querySelector(
".product-entry__sub-title, strong.product-entry__sub-title",
);
if (subtitleEl && product.subtitle)
subtitleEl.textContent = getDisplaySubtitle(product);
var salePrice = product.sale_price || product.price;
var regularPrice = product.regular_price;
buildPriceBlock(salePrice, regularPrice);
var currencyIcon = ' ';
var stickyTotal = document.querySelectorAll(".total-price-single");
var stickyBefore = document.querySelectorAll(".before-price-single");
var stickyDis = document.querySelectorAll(".dis-amount-single");
stickyTotal.forEach(function (el) {
el.innerHTML = salePrice + " " + currencyIcon;
});
stickyBefore.forEach(function (el) {
if (regularPrice && regularPrice !== salePrice) {
el.innerHTML = regularPrice + " " + currencyIcon;
el.style.display = "";
} else {
el.style.display = "none";
}
});
if (stickyDis.length && regularPrice && regularPrice !== salePrice) {
stickyDis.forEach(function (el) {
el.innerHTML = regularPrice - salePrice + " " + currencyIcon;
});
}
if (product.images && product.images.length)
updateSliderImages(product.images, cleanName);
var addBtns = document.querySelectorAll("[product-id]");
addBtns.forEach(function (btn) {
btn.setAttribute("product-id", product.id);
btn.setAttribute("amount", salePrice);
});
var qtyInputs = document.querySelectorAll("[cart-item-id]");
qtyInputs.forEach(function (el) {
el.setAttribute("cart-item-id", product.id);
});
var hiddenId = document.querySelector('input[name="id"]');
if (hiddenId) hiddenId.value = product.id;
var tamaraWidget = document.querySelector("tamara-widget");
if (tamaraWidget) tamaraWidget.setAttribute("amount", salePrice);
var sallaInstallment = document.querySelector("salla-installment");
if (sallaInstallment) sallaInstallment.setAttribute("price", salePrice);
var stickyAddBtns = document.querySelectorAll("salla-add-product-button");
stickyAddBtns.forEach(function (el) {
el.setAttribute("amount", salePrice);
el.setAttribute("product-id", product.id);
});
state.currentProductId = String(product.id);
// Description now lives inside the accordion built by the description
// script, with its first image pulled OUTSIDE into
// .roani-desc-image-wrapper (mirrors what injectDescription() did on
// first load). When swapping in a new product's description we must
// repeat that same split: pull the new first image out into the
// outside wrapper, put the rest (without that image) inside
// .roani-desc-body, and re-run the same empty-paragraph cleanup that
// ran on first load (otherwise runs of
from the new
// product's description are left uncleaned).
if (product.description) {
var tempDiv = document.createElement("div");
tempDiv.innerHTML = product.description;
var newFirstImage = tempDiv.querySelector("img");
var outsideImageWrapper = document.querySelector(
".roani-desc-image-wrapper",
);
if (newFirstImage) {
newFirstImage.remove(); // keep it out of tempDiv so it doesn't duplicate inside the accordion body
if (outsideImageWrapper) {
outsideImageWrapper.innerHTML = "";
outsideImageWrapper.appendChild(newFirstImage);
outsideImageWrapper.style.display = "";
}
} else if (outsideImageWrapper) {
// This product's description has no image: hide the leftover wrapper/image
// instead of showing a stale image from the previous product.
outsideImageWrapper.style.display = "none";
}
var descBodyEl = document.querySelector(".roani-desc-body");
if (descBodyEl) {
descBodyEl.innerHTML = tempDiv.innerHTML;
removeConsecutiveEmptyParagraphsInBody(descBodyEl);
} else {
// Fallback: accordion script hasn't run yet (race on very first load).
var legacyDescEl = document.querySelector("#more-content");
if (legacyDescEl) legacyDescEl.innerHTML = product.description;
}
}
var descSubtitleEl = document.querySelector(".roani-desc-subtitle");
if (descSubtitleEl && product.subtitle) {
descSubtitleEl.innerHTML = getDisplaySubtitle(product);
}
var photoGallery = document.querySelector(
"show-home-component[component-path='saji-photo-gallery']",
);
if (photoGallery) photoGallery.setAttribute("current-prod-id", product.id);
var staticData = getStaticData(product.id);
buildStaticInfoBlock(staticData);
updateActiveCard(product.id);
var breadcrumbList = document.querySelector(
"salla-breadcrumb ol.s-breadcrumb-wrapper",
);
if (breadcrumbList) {
var breadcrumbItems = breadcrumbList.querySelectorAll(
"li.s-breadcrumb-item",
);
if (breadcrumbItems.length) {
var lastItem = breadcrumbItems[breadcrumbItems.length - 1];
lastItem.textContent = cleanName;
}
}
refreshRatingsAndComments(product.id);
// Update the "ماذا يأتي مع شمعة ..." video section (name + video), if present on this page.
updateVideoSection(product);
}
function updateSliderImages(images, productName) {
var mainSlides = document.querySelectorAll(
".s-slider-container .swiper-slide",
);
mainSlides.forEach(function (slide, i) {
var img = slide.querySelector("img");
var link = slide.querySelector("a[data-type='image']");
if (images[i]) {
if (img) {
img.src = images[i].url;
img.setAttribute("data-src", images[i].url);
img.alt = productName || "";
}
if (link) {
link.href = images[i].url;
link.setAttribute("data-caption", productName || "");
}
}
});
var thumbSlides = document.querySelectorAll(
".s-slider-thumbs .swiper-slide",
);
thumbSlides.forEach(function (slide, i) {
var bg = slide.querySelector(".lazy__bg");
if (bg && images[i]) {
bg.setAttribute("data-bg", images[i].url);
bg.style.backgroundImage = 'url("' + images[i].url + '")';
}
});
}
// ==================== UPDATE ACTIVE CARD ====================
function updateActiveCard(activeProductId) {
// Includes both variant cards (.roani-vcard) and bundle cards (.roani-bcard)
// so the active highlight also works when switching bundles in place on a bundle page.
var allVariantCards = document.querySelectorAll(
".roani-vcard, .roani-bcard",
);
allVariantCards.forEach(function (c) {
if (String(c.dataset.productId) === String(activeProductId)) {
c.classList.add("roani-active");
if (window.innerWidth <= 768) {
try {
c.scrollIntoView({
behavior: "smooth",
inline: "center",
block: "nearest",
});
} catch (e) {
/* legacy */
}
}
} else {
c.classList.remove("roani-active");
}
});
}
// ==================== BUILD CARD ====================
function buildCard(p, isBundle) {
var currentId = state.currentProductId;
var displayName = getDisplayName(p);
var isAr = LANG.isAr();
var card = document.createElement("div");
card.className =
(isBundle ? "roani-bcard" : "roani-vcard") +
(String(p.id) === String(currentId) ? " roani-active" : "");
card.dataset.productId = p.id;
card.style.cursor = "pointer";
// Bundle cards now use the natural/first image, same as variant cards.
var imgSrc;
if (isBundle) {
imgSrc =
p.images && p.images[0] ? p.images[0].url : p.image ? p.image.url : "";
} else {
imgSrc =
p.images && p.images[0] ? p.images[0].url : p.image ? p.image.url : "";
}
var img = document.createElement("img");
img.src = imgSrc;
img.alt = displayName;
img.loading = "lazy";
card.appendChild(img);
var info = document.createElement("div");
info.className = isBundle ? "roani-bcard-info" : "roani-vcard-info";
var nameEl = document.createElement("div");
nameEl.className = isBundle ? "roani-bcard-name" : "roani-vcard-name";
if (!isBundle && p.promotion_title) {
var dot = document.createElement("span");
dot.className = "roani-bestseller";
nameEl.appendChild(dot);
}
nameEl.appendChild(document.createTextNode(displayName));
info.appendChild(nameEl);
if (isBundle && p.subtitle) {
var subEl = document.createElement("div");
subEl.className = "roani-bcard-sub";
subEl.textContent = getDisplaySubtitle(p);
info.appendChild(subEl);
}
if (!isBundle) {
var staticInfo = getStaticData(p.id);
if (staticInfo) {
var notesArr = isAr ? staticInfo.notes : staticInfo.notes_en;
if (notesArr && notesArr.length) {
var notesEl = document.createElement("div");
notesEl.className = "roani-vcard-notes";
notesEl.textContent = notesArr.slice(0, 2).join(" • ");
info.appendChild(notesEl);
}
}
}
var salePrice = p.sale_price || p.price;
var regularPrice = p.regular_price;
var currencyLabel = isAr ? "ر.س" : "SAR";
if (isBundle) {
var priceWrap = document.createElement("div");
priceWrap.className = "roani-bcard-price-wrap";
var saleEl = document.createElement("span");
saleEl.className = "roani-bcard-sale";
saleEl.textContent = salePrice + " " + currencyLabel;
priceWrap.appendChild(saleEl);
if (regularPrice && regularPrice !== salePrice) {
var beforeEl = document.createElement("span");
beforeEl.className = "roani-bcard-before";
beforeEl.textContent = regularPrice + " " + currencyLabel;
priceWrap.appendChild(beforeEl);
var saveAmount = regularPrice - salePrice;
var discountPct = Math.round((saveAmount / regularPrice) * 100);
var badge = document.createElement("span");
badge.className = "roani-bcard-badge";
badge.textContent =
(isAr ? "خصم " : "") + discountPct + "%" + (isAr ? "" : " Off");
priceWrap.appendChild(badge);
}
info.appendChild(priceWrap);
} else {
var priceEl = document.createElement("div");
priceEl.className = "roani-vcard-price";
priceEl.textContent = salePrice + " " + currencyLabel;
info.appendChild(priceEl);
}
card.appendChild(info);
if (isBundle) {
var bundleId = String(p.id);
var addBtnLabel = isAr ? "أضف للسلة" : "Add to Cart";
var addedLabel = isAr ? "تمت الإضافة" : "Added";
var addBtn = document.createElement("button");
addBtn.type = "button";
addBtn.className = "roani-bcard-addbtn";
addBtn.setAttribute("aria-label", addBtnLabel);
addBtn.setAttribute("data-bundle-id", bundleId);
addBtn.innerHTML = [
'',
'',
' ',
' ',
' ',
" ",
'' + addBtnLabel + " ",
" ",
'',
'',
' ',
" ",
" ",
'',
'',
' ',
" ",
'' + addedLabel + " ",
" ",
].join("");
addBtn.addEventListener("click", function (event) {
event.stopPropagation();
if (
this.classList.contains("is-loading") ||
this.classList.contains("is-success")
)
return;
var idToAdd = this.getAttribute("data-bundle-id");
this.classList.add("is-loading");
this.disabled = true;
sallaCartAdd(idToAdd, this);
});
card.appendChild(addBtn);
}
card.addEventListener("click", function (e) {
if (e.target.closest(".roani-bcard-addbtn")) return;
if (!isBundle) {
// Variant ("روائح مشابهة") cards always update the current page in place.
document.querySelectorAll(".roani-vcard").forEach(function (c) {
c.classList.remove("roani-active");
});
card.classList.add("roani-active");
updatePageWithProduct(p);
smoothScrollToTop();
return;
}
// Bundle ("باقات مميزة") cards:
if (state.isBundlePage) {
// We're already on a bundle's own page -> just update content in place, no navigation/reload.
updatePageWithProduct(p);
smoothScrollToTop();
} else {
// We're on a single product page -> navigate to the bundle's own page.
var bundleUrl = getProductUrl(p);
if (bundleUrl) {
window.location.href = bundleUrl;
} else {
console.error(
"Roani: could not resolve bundle url for product",
p.id,
);
}
}
});
return card;
}
// ==================== BUILD VARIANTS GRID ====================
function buildVariantsGrid(products) {
var targetDiv = document.querySelector(".saji-custom-div");
if (!targetDiv) return;
products = sortByIdOrder(products, CONFIG.VARIANT_ORDER);
var existing = document.querySelector(".roani-variants-wrap");
if (existing) existing.remove();
var isAr = LANG.isAr();
var wrap = document.createElement("div");
wrap.className = "roani-variants-wrap " + CONFIG.VARIANT_CLASS;
var header = document.createElement("div");
header.className = "roani-variants-header";
var label = document.createElement("p");
label.className = "roani-variants-label";
label.textContent = isAr ? "روائح مختلفة" : "Different Scents";
var count = document.createElement("span");
count.className = "roani-variants-count";
count.textContent = products.length + " " + (isAr ? "روائح" : "Scents");
header.appendChild(label);
header.appendChild(count);
wrap.appendChild(header);
var grid = document.createElement("div");
grid.className = "roani-variants-grid";
state.currentVariantIds = [];
products.forEach(function (p) {
state.currentVariantIds.push(String(p.id));
grid.appendChild(buildCard(p, false));
});
wrap.appendChild(grid);
targetDiv.parentNode.insertBefore(wrap, targetDiv.nextSibling);
state.variantInjected = true;
if (window.innerWidth <= 768) {
setTimeout(function () {
var activeCard = grid.querySelector(".roani-vcard.roani-active");
if (activeCard) {
try {
activeCard.scrollIntoView({
behavior: "smooth",
inline: "center",
block: "nearest",
});
} catch (e) {
/* legacy */
}
}
}, 200);
}
if (!document.querySelector("." + CONFIG.BUNDLES_CLASS)) injectBundles();
}
// ==================== BUILD BUNDLES GRID ====================
// fallbackAnchor: used when there is no ".roani-variants-wrap" on the page
// (i.e. we're on a bundle's own page, where the "روائح مشابهة" section is
// intentionally never built) so the bundles grid still has somewhere to attach.
function buildBundlesGrid(products, fallbackAnchor) {
var variantsWrap = document.querySelector(".roani-variants-wrap");
var anchor = variantsWrap || fallbackAnchor;
if (!anchor) return;
var existing = document.querySelector(".roani-bundles-wrap");
if (existing) existing.remove();
var wrap = document.createElement("div");
wrap.className = "roani-bundles-wrap " + CONFIG.BUNDLES_CLASS;
var label = document.createElement("p");
label.className = "roani-bundles-label";
label.textContent = LANG.isAr() ? "باقات مميزة" : "Featured Bundles";
wrap.appendChild(label);
var grid = document.createElement("div");
grid.className = "roani-bundles-grid";
products.forEach(function (p) {
grid.appendChild(buildCard(p, true));
});
wrap.appendChild(grid);
anchor.parentNode.insertBefore(wrap, anchor.nextSibling);
state.bundlesInjected = true;
}
// ==================== INJECT BUNDLES ====================
function injectBundles() {
if (document.querySelector("." + CONFIG.BUNDLES_CLASS)) return;
var variantsWrap = document.querySelector(".roani-variants-wrap");
if (!variantsWrap) return;
fetchVariantsData(CONFIG.BUNDLE_IDS, function (products) {
if (!products.length) return;
buildBundlesGrid(products);
});
}
// ==================== INJECT VARIANTS ====================
function tryBuildInitialPriceBlock() {
var prices = readCurrentPrices();
if (!prices.sale) return false;
return buildPriceBlock(prices.sale, prices.before || null);
}
function injectVariants() {
if (document.querySelector("." + CONFIG.VARIANT_CLASS)) return;
var targetDiv = document.querySelector(".saji-custom-div");
if (!targetDiv) return;
if (state.currentProductId === null) {
state.currentProductId = getCurrentProductId();
state.isBundlePage =
CONFIG.BUNDLE_IDS.indexOf(String(state.currentProductId)) !== -1;
}
// On a bundle's own page: show ONLY "باقات مميزة", never "روائح مشابهة".
if (state.isBundlePage) {
if (document.querySelector("." + CONFIG.BUNDLES_CLASS)) {
state.variantInjected = true;
return;
}
fetchVariantsData(CONFIG.BUNDLE_IDS, function (products) {
if (!products.length) return;
buildBundlesGrid(products, targetDiv);
state.variantInjected = true;
cleanMainProductTitle();
});
return;
}
// Regular single-product page: show "روائح مشابهة" (and bundles after it, as before).
var staticIds = STATIC_DATA.map(function (item) {
return String(item.id);
});
if (!staticIds.length) return;
fetchVariantsData(staticIds, function (products) {
if (!products.length) return;
state.variantsData = products;
buildVariantsGrid(products);
tryBuildInitialPriceBlock();
var staticData = getStaticData(state.currentProductId);
buildStaticInfoBlock(staticData);
cleanMainProductTitle();
if (state.currentProductId)
refreshRatingsAndComments(state.currentProductId);
// Also sync the video section on initial load (not just on card clicks).
var currentProductData = null;
for (var i = 0; i < products.length; i++) {
if (String(products[i].id) === String(state.currentProductId)) {
currentProductData = products[i];
break;
}
}
if (currentProductData) updateVideoSection(currentProductData);
});
}
// ==================== INIT ====================
function init() {
state.interval = setInterval(function () {
state.tries++;
inject();
if (!state.isInjected && state.tries < CONFIG.MAX_TRIES) return;
clearInterval(state.interval);
}, CONFIG.CHECK_INTERVAL);
state.uspInterval = setInterval(function () {
state.uspTries++;
injectUSP();
if (!state.uspInjected && state.uspTries < CONFIG.MAX_TRIES) return;
clearInterval(state.uspInterval);
}, CONFIG.CHECK_INTERVAL);
state.titleRowInterval = setInterval(function () {
state.titleRowTries++;
injectTitleRow();
if (!state.titleRowInjected && state.titleRowTries < CONFIG.MAX_TRIES)
return;
clearInterval(state.titleRowInterval);
}, CONFIG.CHECK_INTERVAL);
state.variantInterval = setInterval(function () {
state.variantTries++;
injectVariants();
if (!state.variantInjected && state.variantTries < CONFIG.MAX_TRIES)
return;
clearInterval(state.variantInterval);
}, CONFIG.CHECK_INTERVAL);
state.bundlesInterval = setInterval(function () {
state.bundlesTries++;
if (document.querySelector(".roani-variants-wrap")) injectBundles();
if (!state.bundlesInjected && state.bundlesTries < CONFIG.MAX_TRIES)
return;
clearInterval(state.bundlesInterval);
}, CONFIG.CHECK_INTERVAL);
var priceTries = 0;
var priceInterval = setInterval(function () {
priceTries++;
var done = tryBuildInitialPriceBlock();
if (done || priceTries >= CONFIG.MAX_TRIES) clearInterval(priceInterval);
}, CONFIG.CHECK_INTERVAL);
var titleObserverTries = 0;
var titleObserverInterval = setInterval(function () {
titleObserverTries++;
var titleEl = document.querySelector(".product-title");
if (titleEl) {
clearInterval(titleObserverInterval);
var observer = new MutationObserver(function () {
var currentId = state.currentProductId || getCurrentProductId();
var staticData = currentId ? getStaticData(currentId) : null;
var cleaned;
if (staticData) {
cleaned = LANG.isAr() ? staticData.name_ar : staticData.name_en;
} else if (
!LANG.isAr() &&
currentId &&
BUNDLE_NAMES_EN[String(currentId)]
) {
cleaned = BUNDLE_NAMES_EN[String(currentId)];
} else {
cleaned = cleanProductName(titleEl.textContent);
}
if (cleaned && cleaned !== titleEl.textContent.trim()) {
observer.disconnect();
titleEl.textContent = cleaned;
observer.observe(titleEl, {
childList: true,
characterData: true,
subtree: true,
});
}
});
observer.observe(titleEl, {
childList: true,
characterData: true,
subtree: true,
});
cleanMainProductTitle();
} else if (titleObserverTries >= CONFIG.MAX_TRIES) {
clearInterval(titleObserverInterval);
}
}, CONFIG.CHECK_INTERVAL);
}
init();
})();
// -----------------------
(function () {
"use strict";
var TARGET_ID = "p1143856518";
if (window.location.href.indexOf(TARGET_ID) === -1) {
return;
}
function isArLang() {
var htmlLang = (
document.documentElement.getAttribute("lang") || ""
).toLowerCase();
if (htmlLang.indexOf("en") === 0) return false;
if (htmlLang.indexOf("ar") === 0) return true;
var dir = (
document.documentElement.getAttribute("dir") || ""
).toLowerCase();
if (dir === "ltr") return false;
return true;
}
var CONFIG = {
LABEL_CLASS: "roani-weight-label",
LABEL_TEXT_AR: "٤٠٠ جم",
LABEL_TEXT_EN: "400g",
CHECK_INTERVAL: 500,
MAX_TRIES: 50,
};
var state = {
isInjected: false,
tries: 0,
interval: null,
};
var style = document.createElement("style");
style.textContent = [
".s-slider-thumbs .swiper-slide {",
" aspect-ratio: 1 / 1 !important;",
" height: auto !important;",
"}",
".s-slider-thumbs .swiper-slide .lazy__bg {",
" background-size: cover !important;",
" background-position: center !important;",
"}",
".details-slider-wrapper {",
" position: relative;",
"}",
"." + CONFIG.LABEL_CLASS + " {",
" position: absolute;",
" top: 24px;",
" left: 20px;",
" z-index: 20;",
" background: #1a1915;",
" color: #e6e5e1;",
" font-family: 'IBM Plex Sans Arabic', sans-serif;",
" font-size: 12px;",
" font-weight: 700;",
" padding: 5px 10px;",
" border-radius: 6px;",
" line-height: 1.4;",
" pointer-events: none;",
" animation: roaniLabelPop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;",
" transform-origin: top left;",
"}",
"@keyframes roaniLabelPop {",
" 0% { opacity: 0; transform: scale(0.6); }",
" 100% { opacity: 1; transform: scale(1); }",
"}",
].join("\n");
document.head.appendChild(style);
function inject() {
var wrapper = document.querySelector(".details-slider-wrapper");
if (!wrapper) return false;
if (wrapper.querySelector("." + CONFIG.LABEL_CLASS)) {
state.isInjected = true;
return true;
}
var label = document.createElement("span");
label.className = CONFIG.LABEL_CLASS;
label.textContent = isArLang()
? CONFIG.LABEL_TEXT_AR
: CONFIG.LABEL_TEXT_EN;
wrapper.appendChild(label);
state.isInjected = true;
return true;
}
function init() {
state.interval = setInterval(function () {
state.tries++;
var success = inject();
var limitReached = state.tries >= CONFIG.MAX_TRIES;
if (success || limitReached) {
clearInterval(state.interval);
state.interval = null;
}
}, CONFIG.CHECK_INTERVAL);
}
init();
})();
// -----------------------
"use strict";
(function () {
var CONFIG = {
SUBTITLE_SELECTOR: ".product-entry__sub-title",
DESCRIPTION_SELECTOR: "#more-content",
ACCORDION_TITLE: "وصف المنتج",
INJECTED_CLASS: "roani-desc-accordion-injected",
IMAGE_WRAPPER_CLASS: "roani-desc-image-wrapper",
ACTIVE_CLASS: "active",
OPTIONS_TRIGGER_SELECTOR: "#options-panel-trigger",
OPTIONS_MENU_SELECTOR: "#options-menu",
HIDDEN_CLASS: "roani-options-trigger-hidden",
MAX_TRIES: 50,
CHECK_INTERVAL: 500,
MOBILE_BREAKPOINT: 768
};
var state = {
isDescInjected: false,
isOptionsChecked: false,
tries: 0,
interval: null
};
function isMobile() {
return window.innerWidth <= CONFIG.MOBILE_BREAKPOINT;
}
function isEmptyParagraph(el) {
if (!el || el.nodeType !== 1 || el.tagName !== "P") return false;
var html = el.innerHTML.trim().toLowerCase();
var text = el.textContent.trim();
return (html === " " || html === "" || html === " ") && text === "";
}
function removeConsecutiveEmptyParagraphs(container) {
var children = Array.prototype.slice.call(container.children);
var previousWasEmpty = false;
for (var i = 0; i < children.length; i++) {
var el = children[i];
var currentIsEmpty = isEmptyParagraph(el);
if (currentIsEmpty && previousWasEmpty) {
el.remove();
} else {
previousWasEmpty = currentIsEmpty;
}
}
}
function toggleAccordion(button, content) {
var isActive = button.classList.contains(CONFIG.ACTIVE_CLASS);
if (isActive) {
button.classList.remove(CONFIG.ACTIVE_CLASS);
content.classList.remove(CONFIG.ACTIVE_CLASS);
content.style.height = "0px";
content.style.opacity = "0";
} else {
button.classList.add(CONFIG.ACTIVE_CLASS);
content.classList.add(CONFIG.ACTIVE_CLASS);
content.style.height = content.scrollHeight + "px";
content.style.opacity = "1";
}
}
function buildAccordion(subtitleEl, descEl) {
var wrapper = document.createElement("div");
wrapper.className =
CONFIG.INJECTED_CLASS +
" accordion-content-all border mb-4 last:mb-0 [&_.active]:opacity-100 [&_.active]:!h-auto [&_.active]:!border-t [&_.active]:py-3 [&_.active]:px-5 [&_.active]:border-dashed border-primary [&_.active]:border-primary h-fit saji_anime rounded-lg overflow-hidden";
var button = document.createElement("button");
button.type = "button";
button.className =
"accordion text-base !leading-normal flex justify-between rtl:text-right ltr:text-left items-center border-none w-full gap-2 outline-none cursor-pointer text-[var(--store-title)] [background:var(--store-background-secondary)] py-3 px-5";
button.innerHTML =
CONFIG.ACCORDION_TITLE +
' ';
var content = document.createElement("div");
content.className =
"accordion-content overflow-hidden h-0 opacity-0 transition-all duration-200 ease-in-out group text-[var(--store-description)] [background:var(--store-background-secondary)] border-primary";
// subtitle & description now live in stable, always-queryable wrappers
// (.roani-desc-subtitle / .roani-desc-body) so updatePageWithProduct()
// can find and update them later, even after descEl/subtitleEl themselves
// have been removed from the DOM.
var subtitleWrap = document.createElement("p");
subtitleWrap.className =
"roani-desc-subtitle text-sm leading-6 relative mb-3 font-bold";
if (subtitleEl) subtitleWrap.innerHTML = subtitleEl.innerHTML;
var bodyWrap = document.createElement("div");
bodyWrap.className = "roani-desc-body";
bodyWrap.innerHTML = descEl.innerHTML;
removeConsecutiveEmptyParagraphs(bodyWrap);
content.appendChild(subtitleWrap);
content.appendChild(bodyWrap);
button.addEventListener("click", function (e) {
e.preventDefault();
e.stopPropagation();
toggleAccordion(button, content);
});
wrapper.appendChild(button);
wrapper.appendChild(content);
return wrapper;
}
function injectDescription() {
if (state.isDescInjected) return true;
if (document.querySelector("." + CONFIG.INJECTED_CLASS)) {
state.isDescInjected = true;
return true;
}
var descEl = document.querySelector(CONFIG.DESCRIPTION_SELECTOR);
if (!descEl) return false;
var subtitleEl = document.querySelector(CONFIG.SUBTITLE_SELECTOR);
var firstImage = descEl.querySelector("img");
var imageWrapper = null;
if (firstImage) {
imageWrapper = document.createElement("div");
imageWrapper.className = CONFIG.IMAGE_WRAPPER_CLASS + " mb-4";
firstImage.remove();
imageWrapper.appendChild(firstImage);
}
var accordion = buildAccordion(subtitleEl, descEl);
if (imageWrapper) {
descEl.parentNode.insertBefore(imageWrapper, descEl);
}
descEl.parentNode.insertBefore(accordion, descEl);
descEl.remove();
if (subtitleEl) subtitleEl.remove();
state.isDescInjected = true;
return true;
}
function hideEmptyOptionsPanel() {
if (state.isOptionsChecked) return true;
var triggerEl = document.querySelector(CONFIG.OPTIONS_TRIGGER_SELECTOR);
var menuEl = document.querySelector(CONFIG.OPTIONS_MENU_SELECTOR);
if (!triggerEl || !menuEl) return false;
if (menuEl.children.length === 0) {
triggerEl.style.display = "none";
triggerEl.classList.add(CONFIG.HIDDEN_CLASS);
}
state.isOptionsChecked = true;
return true;
}
function startPolling() {
state.interval = setInterval(function () {
state.tries++;
var descDone = injectDescription();
var optionsDone = hideEmptyOptionsPanel();
if ((descDone && optionsDone) || state.tries >= CONFIG.MAX_TRIES) {
clearInterval(state.interval);
}
}, CONFIG.CHECK_INTERVAL);
}
function init() {
startPolling();
}
init();
})();
' + (comment.name || "") + "
", comment.has_order ? [ '' + formatDate(comment.created_at) + "
", '" + (comment.content || comment.text || "") + "
", "