/* 5) RELATED PRODUCTS POPUP V3 */
(function () {
if (window.__tudorsRelatedPopupV3Loaded) return;
window.__tudorsRelatedPopupV3Loaded = true;
const relatedCache = new Map();
const productDetailCache = new Map();
const reviewCache = new Map();
const detailRefsCache = new Map();
function getProductId(button) {
const onclick = button?.getAttribute("onclick") || "";
const match = onclick.match(
/getRelatedProducts\s*\(\s*(\d+)/i
);
if (match) {
return Number(match[1]) || 0;
}
const item = button?.closest(".productItem");
return Number(
item?.querySelector(".productDetail")?.getAttribute("data-id") ||
item?.querySelector(".productName")?.getAttribute("data-id") ||
0
);
}
function getProductUrl(button) {
const item = button?.closest(".productItem");
const link = item?.querySelector(
".productName a[href], .detailUrl[href], a.detailLink[href]"
);
return link?.getAttribute("href") || "";
}
function requestRelatedProducts(productId) {
if (relatedCache.has(productId)) {
return Promise.resolve(relatedCache.get(productId));
}
return new Promise((resolve, reject) => {
if (!window.ticimaxApi?.product?.getRelatedProducts) {
reject(new Error("getRelatedProducts bulunamadı"));
return;
}
ticimaxApi.product.getRelatedProducts(
{
ProductId: productId,
RelatedProduct: true
},
function (response) {
if (
!response ||
response.isError ||
!Array.isArray(response.relatedProducts)
) {
reject(
new Error(
response?.errorMessage ||
"İlgili ürünler alınamadı"
)
);
return;
}
relatedCache.set(productId, response.relatedProducts);
resolve(response.relatedProducts);
}
);
});
}
/* ÜRÜN DETAY SAYFASINDAN TÜM RENKLERİ OKU */
async function fetchAllRelatedRefs(productUrl) {
if (!productUrl) return [];
const absoluteUrl = new URL(productUrl, location.origin).href;
if (detailRefsCache.has(absoluteUrl)) {
return detailRefsCache.get(absoluteUrl);
}
try {
const response = await fetch(absoluteUrl, {
credentials: "same-origin",
cache: "no-store"
});
if (!response.ok) {
throw new Error("Ürün detay sayfası alınamadı");
}
const html = await response.text();
const doc = new DOMParser().parseFromString(
html,
"text/html"
);
const refs = Array.from(
doc.querySelectorAll(
"#productDetailRelated > a.detailLink[data-id]," +
"#productDetailRelated > a[data-id]"
)
)
.map(a => {
const img = a.querySelector("img");
const special2 = a.querySelector(
"#detailRelatedSpecialField2"
);
const special5 = a.querySelector(
"#detailRelatedSpecialField5"
);
return {
productId:
Number(a.getAttribute("data-id")) || 0,
url:
a.getAttribute("href") || "",
name:
a.getAttribute("title") ||
img?.getAttribute("alt") ||
"",
imageThumbPath:
img?.getAttribute("data-original") ||
img?.getAttribute("src-last") ||
img?.getAttribute("src") ||
"",
imageThumb2Path:
img?.getAttribute("data-second") || "",
specialField2:
special2?.innerHTML?.trim() || "",
specialField5:
special5?.textContent?.trim() || ""
};
})
.filter(x => x.productId > 0);
const unique = [];
const seen = new Set();
for (const ref of refs) {
if (seen.has(ref.productId)) continue;
seen.add(ref.productId);
unique.push(ref);
}
detailRefsCache.set(absoluteUrl, unique);
return unique;
} catch (err) {
console.warn(
"TUDORS popup: tüm renkler ürün detayından okunamadı",
err
);
return [];
}
}
/* EKSİK ÜRÜN KARTINI TİCİMAX'TAN AL */
function unwrapProductListDetail(response) {
let model =
response?.model ??
response?.product ??
response;
if (Array.isArray(model)) {
model =
model.find(x => x && typeof x === "object") ||
null;
}
if (model?.product && typeof model.product === "object") {
model = model.product;
}
return model && typeof model === "object" ? model : null;
}
function requestProductListDetail(productId) {
if (productDetailCache.has(productId)) {
return Promise.resolve(productDetailCache.get(productId));
}
return new Promise(resolve => {
if (!window.ticimaxApi?.product?.getProductListDetail) {
resolve(null);
return;
}
ticimaxApi.product.getProductListDetail(
{
ProductId: productId
},
function (response) {
if (!response || response.isError) {
resolve(null);
return;
}
const model = unwrapProductListDetail(response);
if (model) {
productDetailCache.set(productId, model);
}
resolve(model);
}
);
});
}
/* AYNI ANDA ÇOK FAZLA REQUEST ATMA */
async function mapLimit(items, limit, worker) {
const results = new Array(items.length);
let cursor = 0;
async function run() {
while (true) {
const index = cursor++;
if (index >= items.length) return;
try {
results[index] = await worker(items[index], index);
} catch (e) {
results[index] = null;
}
}
}
const workers = [];
const count = Math.min(limit, items.length);
for (let i = 0; i < count; i++) {
workers.push(run());
}
await Promise.all(workers);
return results;
}
/* API MODEL + DETAY SAYFASI VERİSİNİ BİRLEŞTİR */
function mergeProduct(model, ref) {
const p = model || {};
return {
...p,
productId: Number(
p.productId || ref?.productId || 0
),
variantId: Number(
p.variantId ||
p.defaultVariantId ||
p.mainVariantId ||
0
),
defaultVariantId: Number(
p.defaultVariantId ||
p.variantId ||
p.mainVariantId ||
0
),
name: String(
p.name || p.seoName || ref?.name || ""
),
url: String(
p.url || p.defaultUrl || ref?.url || "#"
),
defaultUrl: String(
p.defaultUrl || p.url || ref?.url || "#"
),
imageThumbPath: String(
p.imageThumbPath || ref?.imageThumbPath || ""
),
imageThumb2Path: String(
p.imageThumb2Path || ref?.imageThumb2Path || ""
),
specialField2:
p.specialField2 ?? ref?.specialField2 ?? "",
specialField5:
p.specialField5 ?? ref?.specialField5 ?? "",
productQuantityBoxStepValue:
Number(p.productQuantityBoxStepValue || 1) || 1,
inStock:
p.inStock !== undefined ? p.inStock : true,
redirectToDetail:
Number(p.redirectToDetail || 0),
asortiGroupId:
Number(p.asortiGroupId || 0),
productPoints:
Number(p.productPoints || 0),
productCommentCount:
Number(p.productCommentCount || 0)
};
}
/* TÜM RENKLERİ OLUŞTUR */
async function loadAllRelatedProducts(productId, productUrl) {
const [firstBatch, refs] = await Promise.all([
requestRelatedProducts(productId),
fetchAllRelatedRefs(productUrl)
]);
if (!refs.length) return firstBatch;
const byId = new Map();
firstBatch.forEach(p => {
const id = Number(p.productId || 0);
if (id) {
byId.set(id, p);
}
});
const missingRefs = refs.filter(
ref => !byId.has(ref.productId)
);
if (missingRefs.length) {
const hydrated = await mapLimit(
missingRefs,
5,
ref => requestProductListDetail(ref.productId)
);
missingRefs.forEach((ref, index) => {
byId.set(
ref.productId,
mergeProduct(hydrated[index], ref)
);
});
}
const ordered = refs.map(
ref => mergeProduct(byId.get(ref.productId), ref)
);
const refIds = new Set(
refs.map(x => x.productId)
);
firstBatch.forEach(p => {
const id = Number(p.productId || 0);
if (id && !refIds.has(id)) {
ordered.push(p);
}
});
return ordered;
}
/* GÖRSELLER */
function collectImages(product) {
const images = [];
const add = url => {
if (!url || images.includes(url)) return;
images.push(url);
};
add(product.imageThumbPath);
add(product.imageThumb2Path);
if (Array.isArray(product.imageList)) {
product.imageList.forEach(img => {
if (typeof img === "string") {
add(img);
} else if (img) {
add(
img.imageThumbPath ||
img.imagePath ||
img.path ||
img.url ||
img.src
);
}
});
}
return images;
}
/* YILDIZ */
function createStars(point) {
let value = Number(point || 0);
if (!Number.isFinite(value)) {
value = 0;
}
if (value > 5) {
value = value / 20;
}
value = Math.max(0, Math.min(5, value));
const rounded = Math.round(value);
let stars = "";
for (let i = 1; i <= 5; i++) {
stars += i <= rounded ? "★" : "☆";
}
return stars;
}
/* ÜRÜN KARTI */
function createProductCard(product) {
const productId = Number(product.productId || 0);
const variantId = Number(
product.variantId ||
product.defaultVariantId ||
0
);
const unique = String(product.productUnique || "");
const url = String(
product.url ||
product.defaultUrl ||
"#"
);
const name = String(product.name || "");
const images = collectImages(product);
const discountPrice =
product.productCartPriceStr ||
product.productPriceOriginalStr ||
product.productSellPriceStr ||
"";
const regularPrice = product.productSellPriceStr || "";
const specialField2 = product.specialField2 || "";
const specialField5 = product.specialField5 || "";
const quantityStep =
Number(product.productQuantityBoxStepValue || 1) || 1;
let imagesHtml = "";
images.forEach((image, index) => {
imagesHtml += `
0 ? 'style="display:none;"' : ""}>
`;
});
if (!imagesHtml) {
imagesHtml = '';
}
const discountIcon =
Number(product.discountRate || 0) > 0
? `
%${Number(product.discountRate)}
İndirim
`
: "";
const newIcon = product.newProduct
? 'Yeni
'
: "";
const opportunityIcon = product.opportunityProduct
? 'Fırsat
'
: "";
const favoriteButton =
productId && unique
? `
`
: "";
let cartButton = "";
if (product.inStock === false) {
cartButton = `
`;
} else if (
productId &&
variantId &&
typeof window.productListAddToCart === "function"
) {
cartButton = `
`;
} else {
cartButton = `
`;
}
return `
`;
}
/* LOADING POPUP */
function openLoadingModal() {
const old = document.getElementById("relatedProduct");
if (old) {
old.remove();
}
if (typeof window.createModal !== "function") {
return false;
}
createModal({
id: "relatedProduct",
width: "80%",
content: `
`
});
return true;
}
/* OWL */
function initPopupOwl(root) {
if (
!window.jQuery ||
!jQuery.fn ||
typeof jQuery.fn.owlCarousel !== "function"
) {
return;
}
jQuery(root)
.find(".popupRelatedOwl")
.each(function () {
const $owl = jQuery(this);
const count = Number($owl.attr("data-slide-count") || 0);
if (count <= 1) {
$owl.find(".imageItem").show();
return;
}
if ($owl.hasClass("owl-loaded")) return;
$owl.find(".imageItem").show();
$owl.owlCarousel({
items: 1,
loop: false,
nav: true,
dots: true,
margin: 0,
mouseDrag: true,
touchDrag: true,
smartSpeed: 300,
autoplay: false
});
});
}
/* DİNAMİK MODÜLLER + SİTE YARDIMCILARI */
function runSiteHelpers(root) {
root.querySelectorAll(".popupProductItem")
.forEach(initAllModules);
if (typeof window.Sepetteindirim === "function") {
setTimeout(() => {
try {
window.Sepetteindirim();
} catch (e) {}
}, 100);
}
if (
typeof window.urunfavoriKontrol === "function" &&
window.jQuery
) {
root.querySelectorAll("[data-popup-product-id]")
.forEach(holder => {
const id = Number(
holder.getAttribute("data-popup-product-id") || 0
);
const fav = holder.querySelector(
".relatedAddFavoriteBtn"
);
if (!id || !fav) return;
try {
window.urunfavoriKontrol(
jQuery(fav),
id,
true
);
} catch (e) {}
});
}
}
/* POPUP RENDER */
function renderPopup(products) {
const popup = document.querySelector(
"#relatedProduct .tudorsEnhancedRelatedPopup"
);
if (!popup) return;
const loading = popup.querySelector(".tudorsRelatedLoading");
const list = popup.querySelector(".ulRelatedProducts");
if (!list) return;
if (!products.length) {
if (loading) {
loading.textContent = "Renk seçenekleri bulunamadı.";
}
return;
}
list.innerHTML = products.map(createProductCard).join("");
if (loading) {
loading.remove();
}
initPopupOwl(popup);
runSiteHelpers(popup);
hydrateReviewCounts(products);
}
/* GERÇEK DEĞERLENDİRME SAYISI */
const reviewQueue = [];
let activeReviewRequests = 0;
function drainReviewQueue() {
while (activeReviewRequests < 4 && reviewQueue.length) {
const job = reviewQueue.shift();
activeReviewRequests++;
job().finally(() => { activeReviewRequests--; drainReviewQueue(); });
}
}
function socialCounter(value) {
const text = typeof value === "string" || typeof value === "number" ? String(value).trim() : "";
return /^\d+(?:[.,]\d+)?B?$/.test(text) && parseFloat(text.replace(",", ".")) > 0 ? text : "";
}
function confirmedRating(r) {
return r && Number.isSafeInteger(r.totalRatingCount) && r.totalRatingCount >= 0 &&
Number.isSafeInteger(r.totalCommentCount) && r.totalCommentCount >= 0 &&
typeof r.averageRating === "number" && Number.isFinite(r.averageRating) &&
r.averageRating >= 0 && r.averageRating <= 5 &&
(r.totalRatingCount === 0 || r.averageRating > 0);
}
function requestReviewCount(product) {
const variantId = Number(product.variantId || product.defaultVariantId);
if (!Number.isSafeInteger(variantId) || variantId <= 0) return Promise.resolve(null);
const key = String(product.productId) + ":" + variantId;
const saved = reviewCache.get(key);
if (saved && (saved.pending || saved.until > Date.now())) return saved.promise;
reviewCache.delete(key);
let resolve;
const promise = new Promise(done => { resolve = done; });
reviewCache.set(key, {pending:true, promise});
reviewQueue.push(async () => {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 8000);
let proof = null;
try {
const response = await fetch("https://micro.collectaction.com/api/trendyol_comment/category", {
method:"POST", credentials:"omit", signal:controller.signal,
headers:{"Content-Type":"application/json"},
body:JSON.stringify({firm_id:"139435", ids:[String(variantId)]})
});
if (!response.ok) throw new Error("Aggregate unavailable");
const data = await response.json();
if (data.success === true && Array.isArray(data.result) && data.result.length === 1) {
const item = data.result[0];
const info = item?.product_info;
if (confirmedRating(item?.comments?.rating) ||
[info?.product_basket, info?.product_favorite, info?.product_pageview].some(socialCounter)) proof = item;
}
} catch (_) { /* Failure is not a verified zero. Retry on reopen. */ }
finally { clearTimeout(timer); }
if (proof) {
reviewCache.set(key, {pending:false, promise, until:Date.now() + 60000});
if (reviewCache.size > 128) {
for (const [cacheKey, entry] of reviewCache) {
if (!entry.pending && cacheKey !== key) { reviewCache.delete(cacheKey); break; }
}
}
} else reviewCache.delete(key);
resolve(proof);
});
drainReviewQueue();
return promise;
}
async function hydrateReviewCounts(products) {
const popup = document.querySelector("#relatedProduct .tudorsEnhancedRelatedPopup");
if (!popup) return;
const run = popup.__tudorsReviewRun = (popup.__tudorsReviewRun || 0) + 1;
const current = () => popup.isConnected && popup.__tudorsReviewRun === run &&
popup === document.querySelector("#relatedProduct .tudorsEnhancedRelatedPopup");
await mapLimit(products, 4, async product => {
if (!current()) return;
const id = Number(product.productId);
if (!Number.isSafeInteger(id) || id <= 0) return;
const proof = await requestReviewCount(product);
if (!current() || !proof) return;
const row = popup.querySelector(`[data-rating-product-id="${id}"]`);
const social = popup.querySelector(`[data-social-product-id="${id}"]`);
const r = proof.comments?.rating;
if (row && confirmedRating(r) && r.totalRatingCount > 0) {
const stars = Array.from({length:5}, (_, i) => {
const width = Math.max(0, Math.min(1, r.averageRating - i)) * 100;
return `☆★`;
}).join("");
row.title = "Kaynak: sitedeki CollectAction / Trendyol entegrasyonu";
row.setAttribute("aria-label", `${r.averageRating} / 5; ${r.totalRatingCount} değerlendirme; ${r.totalCommentCount} yorum`);
row.innerHTML = `` +
`${r.averageRating.toFixed(1)}`;
row.style.setProperty("display", "flex", "important");
row.style.flexWrap = "wrap";
}
const info = proof.product_info;
const lines = [
[socialCounter(info?.product_basket), "kişinin sepetinde", ""],
[socialCounter(info?.product_favorite), "kişi favoriledi", ""],
[socialCounter(info?.product_pageview), "kişi görüntüledi", "Bugün "]
].filter(([value]) => value);
if (social && lines.length) {
social.title = "Mevcut pazaryeri kaynağı; Tudors'un ölçülmüş kullanıcı sayısı değildir.";
social.innerHTML = lines.map(([value, text, prefix]) =>
`${prefix}${escapeHtml(value)} ${text}
`).join("");
social.style.display = "block";
}
});
}